diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39e5b3f6003..d2b5cfaad51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -66,6 +66,7 @@ apps/web/src/locales @bitwarden/team-platform-dev apps/browser/src/vault @bitwarden/team-vault-dev apps/cli/src/vault @bitwarden/team-vault-dev apps/desktop/src/vault @bitwarden/team-vault-dev +apps/web/src/app/shared/components/onboarding @bitwarden/team-vault-dev apps/web/src/app/vault @bitwarden/team-vault-dev libs/angular/src/vault @bitwarden/team-vault-dev libs/common/src/vault @bitwarden/team-vault-dev @@ -91,6 +92,7 @@ libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev libs/billing @bitwarden/team-billing-dev libs/pricing @bitwarden/team-billing-dev +libs/subscription @bitwarden/team-billing-dev bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev ## Platform team files ## @@ -162,9 +164,6 @@ apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofil apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys apps/desktop/desktop_native/ssh_agent @bitwarden/team-autofill-desktop-dev @bitwarden/wg-ssh-keys -apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev -apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev @@ -191,7 +190,9 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev bitwarden_license/bit-web/src/app/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev +libs/user-crypto-management @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +libs/unlock @bitwarden/team-key-management-dev # Node-cryptofunction service libs/node @bitwarden/team-key-management-dev @@ -248,4 +249,3 @@ apps/desktop/native-messaging-test-runner/package-lock.json @bitwarden/team-plat .claude/ @bitwarden/team-ai-sme .github/workflows/respond.yml @bitwarden/team-ai-sme .github/workflows/review-code.yml @bitwarden/team-ai-sme -libs/subscription @bitwarden/team-billing-dev diff --git a/.github/renovate.json5 b/.github/renovate.json5 index b264514e736..1e8f72cd8a0 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -542,6 +542,12 @@ }, // ==================== Special Version Constraints ==================== + { + matchPackageNames: ["actions/create-github-app-token"], + matchFileNames: [".github/workflows/test-browser-interactions.yml"], + allowedVersions: "<= 2.0.3", + description: "Versions after v2.0.3 break the test-browser-interactions workflow. Remediation tracked in PM-28174.", + }, { // Any versions of lowdb above 1.0.0 are not compatible with CommonJS. matchPackageNames: ["lowdb"], diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 6a334e31a18..ef696c70d57 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -291,7 +291,7 @@ jobs: working-directory: browser-source/ - name: Download SDK artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type != 'commercial' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -304,10 +304,28 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type != 'commercial' }} working-directory: browser-source/ run: npm link ../sdk-internal + - name: Download Commercial SDK Artifacts + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type == 'commercial' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: commercial-sdk-internal + repo: bitwarden/sdk-internal + path: commercial-sdk-internal + if_no_artifact_found: fail + + - name: Override Commercial SDK + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type == 'commercial' }} + working-directory: browser-source/ + run: npm link ../commercial-sdk-internal + - name: Check source file size if: ${{ startsWith(matrix.browser.name, 'firefox') }} run: | @@ -493,7 +511,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type != 'commercial' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -506,11 +524,30 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type != 'commercial' }} working-directory: ./ run: | npm link ../sdk-internal + - name: Download Commercial SDK Artifacts + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type == 'commercial' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: commercial-sdk-internal + repo: bitwarden/sdk-internal + path: ../commercial-sdk-internal + if_no_artifact_found: fail + + - name: Override Commercial SDK + if: ${{ inputs.sdk_branch != '' && matrix.license_type.type == 'commercial' }} + working-directory: ./ + run: | + npm link ../commercial-sdk-internal + - name: Build Safari extension run: npm run ${{matrix.license_type.npm_command_prefix}}safari working-directory: apps/browser diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 75820c54977..201c7858feb 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -146,7 +146,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type != 'commercial' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -159,12 +159,31 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type != 'commercial' }} working-directory: ./ run: | ls -l ../ npm link ../sdk-internal + - name: Download Commercial SDK Artifacts + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type == 'commercial' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: commercial-sdk-internal + repo: bitwarden/sdk-internal + path: ../commercial-sdk-internal + if_no_artifact_found: fail + + - name: Override Commercial SDK + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type == 'commercial' }} + working-directory: ./ + run: | + npm link ../commercial-sdk-internal + - name: Build & Package Unix env: _SHORT_RUNNER_OS: ${{ env.SHORT_RUNNER_OS }} @@ -421,7 +440,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type != 'commercial' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -434,12 +453,31 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type != 'commercial' }} working-directory: ./ run: | ls -l ../ npm link ../sdk-internal + - name: Download Commercial SDK Artifacts + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type == 'commercial' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: commercial-sdk-internal + repo: bitwarden/sdk-internal + path: ../commercial-sdk-internal + if_no_artifact_found: fail + + - name: Override Commercial SDK + if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' && matrix.license_type.type == 'commercial' }} + working-directory: ./ + run: | + npm link ../commercial-sdk-internal + - name: Build & Package Windows run: npm run dist:${{ matrix.license_type.build_prefix }}:win --quiet diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 688bd30bfe5..36f8ee01c2c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -187,7 +187,7 @@ jobs: persist-credentials: false - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' }} + if: ${{ inputs.sdk_branch != '' && matrix.license_type != 'commercial' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -199,6 +199,19 @@ jobs: path: sdk-internal if_no_artifact_found: fail + - name: Download Commercial SDK Artifacts + if: ${{ inputs.sdk_branch != '' && matrix.license_type == 'commercial' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: commercial-sdk-internal + repo: bitwarden/sdk-internal + path: commercial-sdk-internal + if_no_artifact_found: fail + - name: Check Branch to Publish env: PUBLISH_BRANCHES: "main,rc,hotfix-rc-web" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index efc8c25fc5e..b50db6e08b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -100,13 +100,13 @@ jobs: persist-credentials: false - name: Install Rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: rustfmt, clippy - name: Install Rust nightly - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: nightly components: rustfmt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0f783bbb36..d3cd64bde11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,7 +101,7 @@ jobs: echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -210,7 +210,7 @@ jobs: persist-credentials: false - name: Install rust - uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable components: llvm-tools diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 34acc9a740c..ab86c4179af 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../tsconfig", "compilerOptions": { - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "moduleResolution": "bundler" }, "exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"], "files": [ diff --git a/apps/browser/package.json b/apps/browser/package.json index 745c9d6f3e3..fa3da23991e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2026.1.0", + "version": "2026.2.0", "scripts": { "build": "npm run build:chrome", "build:bit": "npm run build:bit:chrome", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 78cf90c3555..5768966511d 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "تم نسخ $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "إرسال رابط منسوخ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 6a43475da32..eb3135599f4 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { - "message": "Element arxivdən çıxarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındakı girişlərin olduğu siyahının təsviri." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Risk altında olan saytda Bitwarden avto-doldurma menyusu ilə güclü, unikal parolları cəld yaradın.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə və ayarladığınız parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Bu Send keçidini kopyala və paylaş. Qeyd etdiyiniz şəxslər buna növbəti $TIME$ ərzində baxa bilər.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send keçidi kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 9f4a65e3072..cd48ff09ee4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ скапіяваны", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index a46ad75065e..275dd21d0e9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" - }, - "itemUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "valueCopied": { "message": "Копирано е $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илюстрация на списък с елементи за вписване, които са в риск." }, + "welcomeDialogGraphicAlt": { + "message": "Илюстрация на оформлението на страницата с трезора в Битуорден." + }, "generatePasswordSlideDesc": { "message": "Генерирайте бързо сложна и уникална парола от менюто за автоматично попълване на Битуорден, на уеб сайта, който е в риск.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ часа", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Копирайте и споделете връзката към Изпращането. То ще бъде достъпно за всеки с връзката и паролата, която зададете, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Копирайте и споделете връзка към това Изпращане. То ще може да бъде видяно само от хората, които сте посочили, в рамките на следващите $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Връзката към Изпращането е копирана", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index b46d0664231..8aa07e2ec82 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ অনুলিপিত", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index e81fc637b5c..8ef61e0e63e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 2bd53876953..d524731fb46 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "S'ha copiat $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 1501c7d7c4a..efadf781fc8 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { - "message": "Položka byla odebrána z archivu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mail nebo ověřovací kód" + }, "valueCopied": { "message": "Zkopírováno: $VALUE$", "description": "Value has been copied to the clipboard.", @@ -1691,7 +1691,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustrace seznamu přihlášení, která jsou ohrožená." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrace rozložení stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rychle vygeneruje silné, unikátní heslo s nabídkou automatického vyplňování Bitwarden na ohrožených stránkách.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hodin", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Zkopírujte a sdílejte tento odkaz Send. Send bude k dispozici komukoli s odkazem a heslem na dalších $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Zkopírujte a sdílejte tento Send pro odesílání. Můžou jej zobrazit osoby, které jste zadali, a to po dobu $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Odkaz Send byl zkopírován", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 6910fe2efb3..12dc8d7b44f 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod dilysu annilys" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ wedi'i gopïo", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index faf4fc855ec..d19d07ee9f5 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopieret", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link kopieret", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index ad5b45159df..8c33da9ae79 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" - }, - "itemUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "E-Mail oder Verifizierungscode ungültig" + }, "valueCopied": { "message": "$VALUE$ kopiert", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration einer Liste gefährdeter Zugangsdaten." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Generiere schnell ein starkes, einzigartiges Passwort mit dem Bitwarden Auto-Ausfüllen-Menü auf der gefährdeten Website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ Stunden", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiere und teile diesen Send-Link. Das Send wird für jeden mit dem Link und dem von dir festgelegten Passwort für die nächsten $TIME$ verfügbar sein.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiere und teile diesen Send-Link. Er kann von den von dir angegebenen Personen für die nächsten $TIME$ angesehen werden.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-Link kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5964,7 +6006,7 @@ "message": "Kartennummer" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Deine Organisation verwendet keine Master-Passwörter mehr, um sich bei Bitwarden anzumelden. Verifiziere die Organisation und Domain, um fortzufahren." @@ -6124,11 +6166,14 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Bitwarden-Apps herunterladen" }, "emailProtected": { "message": "E-Mail-Adresse geschützt" @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 59f757008f2..0e5fc2eaeb1 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Το στοιχείο στάλθηκε στην αρχειοθήκη" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" - }, - "itemUnarchived": { - "message": "Το στοιχείο επαναφέρθηκε από την αρχειοθήκη" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Αρχειοθέτηση στοιχείου" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ αντιγράφηκε", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ο σύνδεσμος Send αντιγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e77550b01dc..8cbf884072e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -896,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2857,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3080,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6106,6 +6151,15 @@ "searchResults": { "message": "Search results" }, + "simplifiedAutofill": { + "message": "Simplified autofill" + }, + "simplifiedAutofillDescription": { + "message": "When you click a suggested autofill item, it fills rather than taking you to details. You can still view these items from the More menu." + }, + "openSimplifiedAutofillPopover": { + "message": "Open simplified autofill popover" + }, "resizeSideNavigation": { "message": "Resize side navigation" }, @@ -6121,10 +6175,12 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, - "downloadBitwardenApps": { "message": "Download Bitwarden apps" }, @@ -6134,5 +6190,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index e34e20844e6..be564f8a950 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 9fd388a80d3..18e02ec48ca 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index ab5fad7e3af..45d2139fd6b 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" - }, - "itemUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Valor de $VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Genera rápidamente una contraseña segura y única con el menú de autocompletado de Bitwarden en el sitio en riesgo.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Enlace del Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6113,7 +6155,7 @@ "message": "Resize side navigation" }, "whoCanView": { - "message": "Quien puede ver" + "message": "Quién puede ver" }, "specificPeople": { "message": "Personas específicas" @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduce varios correos electrónicos separándolos con una coma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Los individuos tendrán que introducir la contraseña para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index e8efd12b1e2..789454218e1 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ on kopeeritud", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index e7fcd4998e0..f08604910af 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,14 +3,14 @@ "message": "Bitwarden" }, "appLogoLabel": { - "message": "Bitwarden logo" + "message": "Bitwardenen logoa" }, "extName": { "message": "Bitwarden pasahitz kudeatzailea", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Etxean, lanean edo bidean, Bitwardenek zure pasahitz, giltz orokor edo informazio delikatua erraz gordetzen du", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -23,19 +23,19 @@ "message": "Sortu kontua" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Berria Bitwardenen?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Sartu giltz orokorrarekin" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Ireki giltz orokorrarekin" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Erabili SSO" }, "yourOrganizationRequiresSingleSignOn": { - "message": "Your organization requires single sign-on." + "message": "Zure erakundeak SSO erabiltzera behartzen du." }, "welcomeBack": { "message": "Ongi etorri berriro ere" @@ -71,7 +71,7 @@ "message": "Pasahitz nagusia ahazten baduzu, pista batek pasahitza gogoratzen lagunduko dizu." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Zure pasahitza ahazten bazaizu, pasahitzaren pista emailez bidal dezakegu. Gehienez $CURRENT$/$MAXIMUM$ karaktere.", "placeholders": { "current": { "content": "$1", @@ -90,7 +90,7 @@ "message": "Pasahitz nagusirako pista (aukerakoa)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Pasahitzaren sendotasun puntuazioa $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,10 +99,10 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Erakundearen kide egin" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "$ORGANIZATIONNAME$-ren kide egin", "placeholders": { "organizationName": { "content": "$1", @@ -111,7 +111,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Bukatu erakunde honen kide egitea pasahitz nagusi bat ezarriz." }, "tab": { "message": "Fitxak" @@ -138,7 +138,7 @@ "message": "Kopiatu pasahitza" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Kopiatu esaldi-gakoa" }, "copyNote": { "message": "Kopiatu oharra" @@ -159,28 +159,28 @@ "message": "Izena kopiatu" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiatu enpresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiatu segurtasun sozialaren zenbakia" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiatu pasaporte zenbakia" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiatu lizentzia zenbakia" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiatu gako pribatua" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiatu gako publikoa" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiatu hatz-marka" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiatu $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,7 +189,7 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiatu webgunea" }, "copyNotes": { "message": "Kopiatu oharrak" @@ -206,7 +206,7 @@ "message": "Auto-betetzea" }, "autoFillLogin": { - "message": "Autofill login" + "message": "Saio-hasiera autobetetzea" }, "autoFillCard": { "message": "Auto-bete txartela" @@ -261,16 +261,16 @@ "message": "Gehitu elementua" }, "accountEmail": { - "message": "Account email" + "message": "Kontuaren e-maila" }, "requestHint": { - "message": "Request hint" + "message": "Argibidea eskatu" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Pasahitz-laguntza eskatu" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Idatzi zure kontuaren e-maila eta pasahitzaren argibidea bidaliko dizugu" }, "getMasterPasswordHint": { "message": "Jaso pasahitz nagusiaren pista" @@ -297,10 +297,10 @@ "message": "Aldatu pasahitz nagusia" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Web aplikaziora jarraitu?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Esploratu zure Bitwarden kontuaren funtzio gehiago web-aplikazioan." }, "continueToHelpCenter": { "message": "Continue to Help Center?" @@ -332,7 +332,7 @@ "message": "Itxi saioa" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Bitwardeni buruz" }, "about": { "message": "Honi buruz" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Karpeta berria" }, "folderName": { - "message": "Folder name" + "message": "Karpetaren izena" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinkronizatu" }, "syncNow": { - "message": "Sync now" + "message": "Sinkronizatu orain" }, "lastSync": { "message": "Azken sinkronizazioa:" @@ -456,7 +456,7 @@ "message": "Automatikoki pasahitz sendo eta bakarrak sortzen ditu zure saio-hasieratarako." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden web aplikazioa" }, "select": { "message": "Hautatu" @@ -489,11 +489,11 @@ "message": "Luzera" }, "include": { - "message": "Include", + "message": "Sartu", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Sartu letra maiuskulak", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Sartu letra minuskulak", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Sartu zenbakiak", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Sartu karaktere bereziak", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -607,16 +604,16 @@ "message": "Erakutsi" }, "viewAll": { - "message": "View all" + "message": "Ikusi denak" }, "showAll": { - "message": "Show all" + "message": "Dena erakutsi" }, "viewLess": { "message": "View less" }, "viewLogin": { - "message": "View login" + "message": "Ikusi saio-hasiera" }, "noItemsInList": { "message": "Ez dago erakusteko elementurik." @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopiatuta", "description": "Value has been copied to the clipboard.", @@ -946,16 +946,16 @@ "message": "Saioa amaitu da." }, "logIn": { - "message": "Log in" + "message": "Hasi saioa" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Sartu Bitwardenera" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Sartu e-mailera bidali dizugun kodea" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Sartu zure egiaztapenerako aplikazioko kodea" }, "pressYourYubiKeyToAuthenticate": { "message": "Press your YubiKey to authenticate" @@ -1344,11 +1344,11 @@ "message": "Export from" }, "exportVerb": { - "message": "Export", + "message": "Esportatu", "description": "The verb form of the word Export" }, "exportNoun": { - "message": "Export", + "message": "Esportatu", "description": "The noun form of the word Export" }, "importNoun": { @@ -1768,7 +1768,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autobetetzeko iradokizunak" }, "autofillSpotlightTitle": { "message": "Easily find autofill suggestions" @@ -2165,7 +2165,7 @@ "description": "Header for edit file send" }, "viewItemHeaderLogin": { - "message": "View Login", + "message": "Ikusi saio-hasiera", "description": "Header for view login item type" }, "viewItemHeaderCard": { @@ -2203,7 +2203,7 @@ "message": "Bildumak" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ bilduma", "placeholders": { "count": { "content": "$1", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -2928,7 +2931,7 @@ } }, "send": { - "message": "Send", + "message": "Bidali", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4019,7 +4061,7 @@ "message": "required" }, "search": { - "message": "Search" + "message": "Bilatu" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -4346,7 +4388,7 @@ "message": "Select a folder" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Bilduma bat aukeratu" }, "importTargetHintCollection": { "message": "Select this option if you want the imported file contents moved to a collection" @@ -4525,7 +4567,7 @@ "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "Bilduma" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." @@ -4686,7 +4728,7 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Autobetetzeko proposamenak" }, "itemSuggestions": { "message": "Suggested items" @@ -4812,7 +4854,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Esleitu bildumetan" }, "copyEmail": { "message": "Copy email" @@ -4904,7 +4946,7 @@ } }, "new": { - "message": "New" + "message": "Berria" }, "removeItem": { "message": "Remove $NAME$", @@ -4942,10 +4984,10 @@ "message": "Additional information" }, "itemHistory": { - "message": "Item history" + "message": "Aldaketen historia" }, "lastEdited": { - "message": "Last edited" + "message": "Azken edizioa" }, "ownerYou": { "message": "Owner: You" @@ -5029,7 +5071,7 @@ "message": "Filters" }, "filterVault": { - "message": "Filter vault" + "message": "Iragazi kutxa gotorra" }, "filterApplied": { "message": "One filter applied" @@ -5066,13 +5108,13 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Saio-hasierako kredentzialak" }, "authenticatorKey": { "message": "Authenticator key" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autobetetzeko aukerak" }, "websiteUri": { "message": "Website (URI)" @@ -5866,7 +5908,7 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Sortu pasahitzak azkar" }, "generatorNudgeBodyOne": { "message": "Easily create strong and unique passwords by clicking on", @@ -5879,7 +5921,7 @@ "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Sortu erraz pasahitz sendo eta bakarrak Sortu pasahitza botoian klik eginez, zure saio-hasierak seguru mantentzen laguntzeko.", "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { @@ -6107,7 +6149,7 @@ "message": "Items" }, "searchResults": { - "message": "Search results" + "message": "Bilaketaren emaitzak" }, "resizeSideNavigation": { "message": "Resize side navigation" @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index bca4ad20d52..4c2474d614c 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ کپی شد", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "تصویری از فهرست ورودهایی که در معرض خطر هستند." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "با استفاده از منوی پر کردن خودکار Bitwarden در سایت در معرض خطر، به‌سرعت یک کلمه عبور قوی و منحصر به فرد تولید کنید.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "لینک ارسال کپی شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2997ed6c128..8f5d3c05a63 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -29,7 +29,7 @@ "message": "Kirjaudu pääsyavaimella" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Poista lukitus todentamisavaimella" }, "useSingleSignOn": { "message": "Käytä kertakirjautumista" @@ -440,7 +440,7 @@ "message": "Synkronointi" }, "syncNow": { - "message": "Sync now" + "message": "Synkronoi nyt" }, "lastSync": { "message": "Viimeisin synkronointi:" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkistoi kohde" @@ -610,10 +607,10 @@ "message": "Näytä kaikki" }, "showAll": { - "message": "Show all" + "message": "Näytä kaikki" }, "viewLess": { - "message": "View less" + "message": "Näytä vähemmän" }, "viewLogin": { "message": "View login" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopioitu", "description": "Value has been copied to the clipboard.", @@ -1495,7 +1495,7 @@ "message": "This file is using an outdated encryption method." }, "attachmentUpdated": { - "message": "Attachment updated" + "message": "Liite päivitetty" }, "file": { "message": "Tiedosto" @@ -1664,7 +1664,7 @@ "message": "Passkey authentication failed" }, "useADifferentLogInMethod": { - "message": "Use a different log in method" + "message": "Käytä vaihtoehtoista kirjautumistapaa" }, "awaitingSecurityKeyInteraction": { "message": "Odotetaan suojausavaimen aktivointia..." @@ -1956,7 +1956,7 @@ "message": "Erääntymisvuosi" }, "monthly": { - "message": "month" + "message": "kuukausi" }, "expiration": { "message": "Voimassaolo päättyy" @@ -2055,7 +2055,7 @@ "message": "Sähköposti" }, "emails": { - "message": "Emails" + "message": "Sähköpostit" }, "phone": { "message": "Puhelinnumero" @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Kuvitus vaarantuneiden kirjautumistietojen luettelosta." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Luo vahva ja ainutlaatuinen salasana nopeasti Bitwardenin automaattitäytön valikosta vaarantuneella sivustolla.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-linkki kopioitiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4827,7 +4869,7 @@ "message": "Hallintapaneelista" }, "admin": { - "message": "Admin" + "message": "Ylläpitäjä" }, "automaticUserConfirmation": { "message": "Automatic user confirmation" @@ -4857,16 +4899,16 @@ "message": "Turned on automatic confirmation" }, "availableNow": { - "message": "Available now" + "message": "Saatavilla nyt" }, "accountSecurity": { "message": "Tilin suojaus" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Tietojenkalasteluhyökkäysten estäminen" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Tietojenkalasteluhyökkäysten tunnistaminen" }, "enablePhishingDetectionDesc": { "message": "Display warning before accessing suspected phishing sites" @@ -4984,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Lataa liite" }, "downloadBitwarden": { "message": "Lataa Bitwarden" @@ -5719,10 +5761,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Heikko salasana" }, "changeNow": { - "message": "Change now" + "message": "Vaihda nyt" }, "missingWebsite": { "message": "Missing website" @@ -5776,13 +5818,13 @@ "message": "Tervetuloa holviisi!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Havaittu tietojenkalasteluhyökkäyksen yritys" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Sivusto jota olet avaamassa on tunnetusti haitallinen ja sen avaaminen on turvallisuusriski" }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Sulje tämä välilehti" }, "phishingPageContinueV2": { "message": "Jatka tälle sivustolle (ei suositeltavaa)" @@ -5896,10 +5938,10 @@ "description": "'WebAssembly' is a technical term and should not be translated." }, "showMore": { - "message": "Show more" + "message": "Näytä enemmän" }, "showLess": { - "message": "Show less" + "message": "Näytä vähemmän" }, "next": { "message": "Seuraava" @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 11da450cc0f..069be19dc86 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Kinopya ang $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index face33e0087..afd5415e249 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -29,7 +29,7 @@ "message": "Se connecter avec une clé d'accès" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Déverrouiller avec une clé d'accès" }, "useSingleSignOn": { "message": "Utiliser l'authentification unique" @@ -573,32 +573,29 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Élément archivé" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Élément désarchivé" }, "archiveItem": { "message": "Archiver l'élément" }, "archiveItemDialogContent": { - "message": "Once archived, this item will be excluded from search results and autofill suggestions." + "message": "Une fois archivé, cet élément sera exclu des résultats de recherche et des suggestions de remplissage automatique." }, "archived": { - "message": "Archived" + "message": "Archivé" }, "unarchiveAndSave": { - "message": "Unarchive and save" + "message": "Désarchiver et enregistrer" }, "upgradeToUseArchive": { "message": "Une adhésion premium est requise pour utiliser Archive." }, "itemRestored": { - "message": "Item has been restored" + "message": "L'élément a été restauré" }, "edit": { "message": "Modifier" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "valueCopied": { "message": "$VALUE$ copié", "description": "Value has been copied to the clipboard.", @@ -991,10 +991,10 @@ "message": "Non" }, "noAuth": { - "message": "Anyone with the link" + "message": "Toute personne disposant du lien" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "N'importe qui avec un mot de passe défini par vous" }, "location": { "message": "Emplacement" @@ -1321,7 +1321,7 @@ "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choisit la manière dont la détection des correspondances URI est gérée par défaut pour les connexions lors d'actions telles que la saisie automatique." + "message": "Choisissez le mode de traitement par défaut de la détection de correspondance URI, pour les connexions lors de l'exécution d'actions telles que le remplissage automatique." }, "theme": { "message": "Thème" @@ -1344,7 +1344,7 @@ "message": "Exporter à partir de" }, "exportVerb": { - "message": "Export", + "message": "Exporter", "description": "The verb form of the word Export" }, "exportNoun": { @@ -1356,7 +1356,7 @@ "description": "The noun form of the word Import" }, "importVerb": { - "message": "Import", + "message": "Importer", "description": "The verb form of the word Import" }, "fileFormat": { @@ -1555,13 +1555,13 @@ "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, "premiumSubscriptionEnded": { - "message": "Your Premium subscription ended" + "message": "Votre abonnement Premium est terminé" }, "archivePremiumRestart": { - "message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault." + "message": "Pour récupérer l'accès à vos archives, redémarrez votre abonnement Premium. Si vous modifiez les détails d'un élément archivé avant de le redémarrer, il sera déplacé dans votre coffre." }, "restartPremium": { - "message": "Restart Premium" + "message": "Redémarrer Premium" }, "ppremiumSignUpReports": { "message": "Hygiène du mot de passe, santé du compte et rapports sur les brèches de données pour assurer la sécurité de votre coffre." @@ -2055,7 +2055,7 @@ "message": "Courriel" }, "emails": { - "message": "Emails" + "message": "Courriels" }, "phone": { "message": "Téléphone" @@ -2486,7 +2486,7 @@ "message": "Élément définitivement supprimé" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Élément archivé restauré" }, "restoreItem": { "message": "Restaurer l'élément" @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration d'une liste de connexions à risque." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Générez rapidement un mot de passe fort et unique grâce au menu de saisie automatique de Bitwarden sur le site à risque.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ heures", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne disposant du lien pour les prochains $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copiez et partagez ce lien Send. Le Send sera accessible à toute personne possédant le lien et le mot de passe que vous avez défini pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copiez et partagez ce lien Send. Il peut être vu par les personnes définies pendant $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Lien Send copié", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -3353,10 +3395,10 @@ "message": "Erreur" }, "prfUnlockFailed": { - "message": "Failed to unlock with passkey. Please try again or use another unlock method." + "message": "Impossible de déverrouiller avec la clé d'accès. Veuillez réessayer ou utiliser une autre méthode de déverrouillage." }, "noPrfCredentialsAvailable": { - "message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first." + "message": "Aucune clé d'accès PRF disponible pour le déverrouillage. Veuillez vous connecter avec une clé d'accès en premier lieu." }, "decryptionError": { "message": "Erreur de déchiffrement" @@ -4145,7 +4187,7 @@ "message": "Ok" }, "toggleSideNavigation": { - "message": "Basculer la navigation latérale" + "message": "Activer la navigation latérale" }, "skipToContent": { "message": "Accéder directement au contenu" @@ -4734,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "More options" + "message": "Plus de paramètres" }, "moreOptionsTitle": { "message": "Plus d'options - $ITEMNAME$", @@ -4830,46 +4872,46 @@ "message": "Admin" }, "automaticUserConfirmation": { - "message": "Automatic user confirmation" + "message": "Confirmation d'utilisateur automatique" }, "automaticUserConfirmationHint": { - "message": "Automatically confirm pending users while this device is unlocked" + "message": "Confirmer automatiquement les utilisateurs en attente pendant que cet appareil est déverrouillé" }, "autoConfirmOnboardingCallout": { - "message": "Save time with automatic user confirmation" + "message": "Gagnez du temps grâce à la confirmation d'utilisateur automatique" }, "autoConfirmWarning": { - "message": "This could impact your organization’s data security. " + "message": "Cela peut avoir un impact sur la sécurité des données de votre organisation. " }, "autoConfirmWarningLink": { - "message": "Learn about the risks" + "message": "En apprendre plus sur les risques" }, "autoConfirmSetup": { - "message": "Automatically confirm new users" + "message": "Confirmer les nouveaux utilisateurs automatiquement" }, "autoConfirmSetupDesc": { - "message": "New users will be automatically confirmed while this device is unlocked." + "message": "Les nouveaux utilisateurs seront confirmés automatiquement pendant que cet appareil est déverrouillé." }, "autoConfirmSetupHint": { - "message": "What are the potential security risks?" + "message": "Quels-sont les potentiels risques de sécurité ?" }, "autoConfirmEnabled": { - "message": "Turned on automatic confirmation" + "message": "Confirmation automatique activée" }, "availableNow": { - "message": "Available now" + "message": "Disponible maintenant" }, "accountSecurity": { "message": "Sécurité du compte" }, "phishingBlocker": { - "message": "Phishing Blocker" + "message": "Bloqueur d'hameçonnage" }, "enablePhishingDetection": { - "message": "Phishing detection" + "message": "Détection de l'hameçonnage" }, "enablePhishingDetectionDesc": { - "message": "Display warning before accessing suspected phishing sites" + "message": "Afficher un avertissement avant d'accéder à des sites soupçonnés d'hameçonnage" }, "notifications": { "message": "Notifications" @@ -4984,7 +5026,7 @@ } }, "downloadAttachmentLabel": { - "message": "Download Attachment" + "message": "Télécharger la pièce jointe" }, "downloadBitwarden": { "message": "Télécharger Bitwarden" @@ -5126,10 +5168,10 @@ } }, "showMatchDetectionNoPlaceholder": { - "message": "Show match detection" + "message": "Afficher la détection de correspondance" }, "hideMatchDetectionNoPlaceholder": { - "message": "Hide match detection" + "message": "Masquer la détection de correspondance" }, "autoFillOnPageLoad": { "message": "Saisir automatiquement lors du chargement de la page ?" @@ -5365,10 +5407,10 @@ "message": "Emplacement de l'élément" }, "fileSends": { - "message": "Envoi de fichiers" + "message": "Sends de fichier" }, "textSends": { - "message": "Envoi de textes" + "message": "Sends de texte" }, "accountActions": { "message": "Actions du compte" @@ -5668,7 +5710,7 @@ "message": "Très large" }, "narrow": { - "message": "Narrow" + "message": "Réduire" }, "sshKeyWrongPassword": { "message": "Le mot de passe saisi est incorrect." @@ -5719,10 +5761,10 @@ "message": "Cet identifiant est à risques et manque un site web. Ajoutez un site web et changez le mot de passe pour une meilleure sécurité." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Mot de passe vulnérable." }, "changeNow": { - "message": "Change now" + "message": "Changer maintenant" }, "missingWebsite": { "message": "Site Web manquant" @@ -5964,7 +6006,7 @@ "message": "Numéro de carte" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erreur : décryptage impossible" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Votre organisation n'utilise plus les mots de passe principaux pour se connecter à Bitwarden. Pour continuer, vérifiez l'organisation et le domaine." @@ -6095,46 +6137,52 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Accepter le transfert" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Refuser et quitter" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Pourquoi vois-je ceci ?" }, "items": { - "message": "Items" + "message": "Éléments" }, "searchResults": { - "message": "Search results" + "message": "Résultats de la recherche" }, "resizeSideNavigation": { - "message": "Resize side navigation" + "message": "Ajuster la taille de la navigation latérale" }, "whoCanView": { - "message": "Who can view" + "message": "Qui peut visionner" }, "specificPeople": { - "message": "Specific people" + "message": "Personnes spécifiques" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Après avoir partagé ce lien Send, les individus devront vérifier leur courriel à l'aide d'un code afin de voir ce Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Entrez plusieurs courriels en les séparant d'une virgule." + }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer toutes les adresses, changez le type d'accès ci-dessus." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "utilisateur@bitwarden.com , utilisateur@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Télécharger les applications Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Courriel protégé" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Les individus devront entrer le mot de passe pour visionner ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Échec de la vérification d'utilisateur." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 69ef54f78eb..c84e1f56a95 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificación non válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copiado", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ligazón do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 22939259639..5462940fa63 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט שוחזר מהארכיב" - }, - "itemUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "השדה $VALUE$ הועתק לזיכרון", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "איור של רשימת כניסות בסיכון." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "צור במהירות סיסמה חזקה וייחודית עם תפריט המילוי האוטומטי של Bitwarden באתר שבסיכון.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "קישור סֵנְד הועתק", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 298f0312be7..1fe7310a1f5 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "सत्यापन कोड अवैध है" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ कॉपी हो गया है।", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "जोखिमग्रस्त लॉगिन की सूची का चित्रण।" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index d7814a22da0..b73f52ffaf5 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -29,7 +29,7 @@ "message": "Prijava pristupnim ključem" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Otključaj prisutupnim ključem" }, "useSingleSignOn": { "message": "Jedinstvena prijava (SSO)" @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Stavka arhivirana" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Stavka vraćena iz arhive" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " kopirano", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracija liste rizičnih prijava." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Brzo generiraj jake, jedinstvene lozinke koristeći Bitwarden dijalog auto-ispune direktno na stranici.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Kopirana poveznica Senda", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index ec4b2d405bf..5736378a638 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -573,15 +573,12 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Az elem visszavételre került az archivumból." }, - "itemUnarchived": { - "message": "Az elemek visszavéelre kerültek az archivumból." - }, "archiveItem": { "message": "Elem archiválása" }, @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "valueCopied": { "message": "$VALUE$ másolásra került.", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "A kockázatos bejelentkezések listájának illusztrációja." }, + "welcomeDialogGraphicAlt": { + "message": "A Bitwarden széf oldal elrendezésének illusztrációja." + }, "generatePasswordSlideDesc": { "message": "Gyorsan generálhatunk erős, egyedi jelszót a Bitwarden automatikus kitöltési menüjével a kockázatos webhelyen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ óra", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Másoljuk és osszuk meg ezt a Send elem hivatkozást. A Send elem bárki számára elérhető lesz, aki rendelkezik a hivatkozással és a jelszóval a következő $TIME$ alatt.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Másoljuk és osszuk meg ezt a Send hivatkozást. Megtekinthetik a megadott személyek a következő $TIME$ intervallumban.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "A Send hivatkozás másolásra került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index f364b2f7540..63e975466ed 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Butir yang diarsipkan akan muncul di sini dan akan dikecualikan dari hasil pencarian umum dan saran isi otomatis." }, - "itemWasSentToArchive": { - "message": "Butir dikirim ke arsip" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arsipkan butir" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ disalin", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Gambaran daftar info masuk yang berpotensi bahaya." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Hasilkan kata sandi yang kuat dan unik dengan cepat dengan menu isi otomatis Bitwarden pada situs yang berpotensi bahaya.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Tautan Send disalin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 9c4ce6a0369..f1d704c6d48 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati compariranno qui e saranno esclusi dai risultati di ricerca e suggerimenti di autoriempimento." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Elemento archiviato" }, - "itemWasUnarchived": { - "message": "Elemento rimosso dall'archivio" - }, - "itemUnarchived": { - "message": "Elemento rimosso dall'archivio" + "itemUnarchivedToast": { + "message": "Elemento estratto dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Codice di verifica non valido" + }, "valueCopied": { "message": "$VALUE$ copiata", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustrazione di una lista di login a rischio." }, + "welcomeDialogGraphicAlt": { + "message": "Illustrazione del layout di pagina della cassaforte Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Genera rapidamente una parola d'accesso forte e unica con il menu' di riempimento automatico Bitwarden nel sito a rischio.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ ore", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia il link per $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copia e condividi questo link di Send. Sarà disponibile a chiunque abbia link e password per $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copia e condividi questo link Send: potrà essere visualizzato dalle persone che hai specificato per $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link del Send copiato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5964,7 +6006,7 @@ "message": "Numero di carta" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio." @@ -6104,10 +6146,10 @@ "message": "Perché vedo questo avviso?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Risultati di ricerca" }, "resizeSideNavigation": { "message": "Ridimensiona la navigazione laterale" @@ -6124,17 +6166,23 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Scarica l'app Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index c8a963fc744..ecb03f3321d 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "アーカイブされたアイテムはここに表示され、通常の検索結果および自動入力の候補から除外されます。" }, - "itemWasSentToArchive": { - "message": "アイテムはアーカイブに送信されました" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "アイテムはアーカイブから解除されました" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "アイテムをアーカイブ" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ をコピーしました", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "危険な状態にあるログイン情報の一覧表示の例" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Bitwarden の自動入力メニューで、強力で一意なパスワードをすぐに生成しましょう。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send リンクをコピーしました", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index cb6129ed2bb..17c86de13fb 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index e97ce2a95a4..0dd4699a9a2 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ ನಕಲಿಸಲಾಗಿದೆ", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 9f570d62abb..66467d99888 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "보관된 항목은 여기에 표시되며 일반 검색 결과 및 자동 완성 제안에서 제외됩니다." }, - "itemWasSentToArchive": { - "message": "항목이 보관함으로 이동되었습니다" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "항목이 보관 해제되었습니다" - }, - "itemUnarchived": { - "message": "항목 보관 해제됨" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "항목 보관" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$를 클립보드에 복사함", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 링크 복사됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6e105f044f3..a4cfb34942c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Nukopijuota $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 8c86d7040fe..d6e9a171978 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Vienums ievietots arhīvā" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" - }, - "itemUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "valueCopied": { "message": "$VALUE$ ir starpliktuvē", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Riskam pakļauto pieteikšanās vienumu saraksta attēlojums." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Riskam pakļauto vienumu vietnē ar automātiskās aizpildes izvēlni var ātri izveidot stipru, neatkārtojamu paroli.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send saite ievietota starpliktuvē", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 61f69ffe22b..61b9f4d45ab 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ പകർത്തി", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 5cc614c5df7..2a793784aa2 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index ce6c8d5a7d4..d19dc3571e2 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ er kopiert", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-lenken ble kopiert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 44522727429..0d3ed844a15 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" - }, - "itemUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "valueCopied": { "message": "$VALUE$ gekopieerd", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Voorbeeld van een lijst van risicovolle inloggegevens." }, + "welcomeDialogGraphicAlt": { + "message": "Illustratie van de lay-out van de Bitwarden-kluispagina." + }, "generatePasswordSlideDesc": { "message": "Genereer snel een sterk, uniek wachtwoord met het automatisch invulmenu van Bitwarden op de risicovolle website.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ uur", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopieer en deel deze Send-link. De Send is beschikbaar voor iedereen met de link en het ingestelde wachtwoord voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopieer en deel deze Send-link. Het kan worden bekeken door de mensen die je hebt opgegeven voor de volgende $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send-link gekopieerd", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 44c7d9e6d47..c470af8c1dc 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -440,7 +440,7 @@ "message": "Synchronizacja" }, "syncNow": { - "message": "Sync now" + "message": "Synchronizuj teraz" }, "lastSync": { "message": "Ostatnia synchronizacja:" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" - }, - "itemUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Skopiowano $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustracja listy danych logowania, które są zagrożone." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Wygeneruj silne i unikalne hasło dla zagrożonej strony internetowej za pomocą autouzupełniania Bitwarden.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link wysyłki został skopiowany", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,11 +6166,14 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Pobierz aplikacje Bitwarden" }, "emailProtected": { "message": "Email protected" @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 5ad95b480db..9b0c2483b2a 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -991,10 +991,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -2055,7 +2055,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do Cofre do Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gere uma senha forte e única com rapidez com o menu de preenchimento automático no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5964,7 +6006,7 @@ "message": "Número do cartão" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "A sua organização não está mais usando senhas principais para se conectar ao Bitwarden. Para continuar, verifique a organização e o domínio." @@ -6104,37 +6146,43 @@ "message": "Por que estou vendo isso?" }, "items": { - "message": "Items" + "message": "Itens" }, "searchResults": { - "message": "Search results" + "message": "Resultados da busca" }, "resizeSideNavigation": { "message": "Redimensionar navegação lateral" }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Baixar aplicativos do Bitwarden" }, "emailProtected": { - "message": "Email protected" + "message": "Protegido por e-mail" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 604bf054707..5d498908fa5 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "valueCopied": { "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ilustração de uma lista de credenciais que estão em risco." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustração do layout da página do cofre Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Gira rapidamente uma palavra-passe forte e única com o menu de preenchimento automático do Bitwarden no site em risco.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copie e partilhe este link do Send. O Send estará disponível para qualquer pessoa com o link e palavras-passe durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copie e partilhe este link do Send. Pode ser visualizado pelas pessoas que especificou durante os próximos $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Link do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 12706943e83..eb6c7eb2a66 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": " $VALUE$ s-a copiat", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index dab9a22f03a..75a2afb4e1a 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" - }, - "itemUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "valueCopied": { "message": "$VALUE$ скопировано", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Иллюстрация списка логинов, которые подвержены риску." }, + "welcomeDialogGraphicAlt": { + "message": "Иллюстрация макета страницы хранилища Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Быстро сгенерируйте надежный уникальный пароль с помощью меню автозаполнения Bitwarden на сайте, подверженном риску.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ час.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка, в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Скопируйте и поделитесь этой ссылкой Send. Send будет доступна всем, у кого есть ссылка и пароль в течение следующих $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Скопируйте и распространите эту ссылку для Send. Она может быть просмотрена указанными вами пользователями в течение $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Ссылка на Send скопирована", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index d228cdb512a..d7e63d70f87 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "වලංගු නොවන සත්යාපන කේතය" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ පිටපත් කරන ලදි", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index db7efcd8b9f..806e83e0b1f 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { - "message": "Položka bola odobraná z archívu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mailový alebo overovací kód" + }, "valueCopied": { "message": " skopírované", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Príklady zoznamu prihlásení, ktoré sú ohrozené." }, + "welcomeDialogGraphicAlt": { + "message": "Ilustrácia rozloženia stránky trezoru Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Rýchlo generujte silné, jedinečné heslo pomocu ponuky automatického vypĺňania Bitwardenu na ohrozených stránkach.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hod.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Send bude dostupný každému, kto má odkaz a heslo od vás, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Skopírujte a zdieľajte tento odkaz na Send. Zobraziť ho môžu ľudia, ktorých ste vybrali, počas nasledujúcich $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skopírovaný odkaz na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Jednotlivci budú musieť zadať heslo, aby mohli zobraziť tento Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 07ee84ab810..88f54663ccd 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -6,30 +6,30 @@ "message": "Bitwarden logo" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden – Upravitelj gesel", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Doma, na delu ali na poti – Bitwarden enostavno zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Povabilo sprejeto" }, "createAccount": { "message": "Ustvari račun" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Novi na Bitwardenu?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Prijava s ključem za dostop" }, "unlockWithPasskey": { - "message": "Unlock with passkey" + "message": "Odkleni s ključem za dostop" }, "useSingleSignOn": { "message": "Use single sign-on" @@ -38,10 +38,10 @@ "message": "Your organization requires single sign-on." }, "welcomeBack": { - "message": "Welcome back" + "message": "Dobrodošli nazaj" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Nastavite močno geslo" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -90,7 +90,7 @@ "message": "Namig za glavno geslo (neobvezno)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Ocena moči gesla: $SCORE$", "placeholders": { "score": { "content": "$1", @@ -99,7 +99,7 @@ } }, "joinOrganization": { - "message": "Join organization" + "message": "Pridružite se organizaciji" }, "joinOrganizationName": { "message": "Join $ORGANIZATIONNAME$", @@ -156,31 +156,31 @@ "message": "Kopiraj varnostno kodo" }, "copyName": { - "message": "Copy name" + "message": "Kopiraj ime" }, "copyCompany": { - "message": "Copy company" + "message": "Kopiraj podjetje" }, "copySSN": { "message": "Copy Social Security number" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiraj številko potnega lista" }, "copyLicenseNumber": { "message": "Copy license number" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiraj zasebni ključ" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiraj javni ključ" }, "copyFingerprint": { "message": "Copy fingerprint" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiraj $FIELD$", "placeholders": { "field": { "content": "$1", @@ -189,17 +189,17 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiraj spletno stran" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiraj zapiske" }, "copy": { - "message": "Copy", + "message": "Kopiraj", "description": "Copy to clipboard" }, "fill": { - "message": "Fill", + "message": "Izpolni", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -215,10 +215,10 @@ "message": "Samodejno izpolni identiteto" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Izpolni kodo za preverjanje" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Izpolni kodo za preverjanje", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -297,13 +297,13 @@ "message": "Spremeni glavno geslo" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Raziščite več funkcij vašega Bitwarden računa na spletni aplikaciji." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Nadaljuj na center za pomoč?" }, "continueToHelpCenterDesc": { "message": "Learn more about how to use Bitwarden on the Help Center." @@ -315,7 +315,7 @@ "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Vaše glavno geslo lahko zamenjate v Bitwarden spletni aplikaciji." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -332,19 +332,19 @@ "message": "Odjava" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "O Bitwardenu" }, "about": { "message": "O programu" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Več od Bitwardena" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Nadaljuj na bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden za podjetja" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" @@ -398,10 +398,10 @@ } }, "newFolder": { - "message": "New folder" + "message": "Nova mapa" }, "folderName": { - "message": "Folder name" + "message": "Ime mape" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -440,7 +440,7 @@ "message": "Sinhronizacija" }, "syncNow": { - "message": "Sync now" + "message": "Sinhroniziraj zdaj" }, "lastSync": { "message": "Zadnja sinhronizacija:" @@ -456,7 +456,7 @@ "message": "Avtomatično generiraj močna, edinstvena gesla za vaše prijave." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden spletna aplikacija" }, "select": { "message": "Izberi" @@ -468,7 +468,7 @@ "message": "Generate passphrase" }, "passwordGenerated": { - "message": "Password generated" + "message": "Geslo generirano" }, "passphraseGenerated": { "message": "Passphrase generated" @@ -493,7 +493,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Vključi velike črke", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -501,7 +501,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Vključi male črke", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -509,7 +509,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Vključi števila", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -517,7 +517,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Vključi posebne znake", "description": "Full description for the password generator special characters checkbox" }, "numWords": { @@ -540,7 +540,7 @@ "message": "Minimalno posebnih znakov" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Izogibaj se dvoumnim znakom", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -554,33 +554,30 @@ "message": "Reset search" }, "archiveNoun": { - "message": "Archive", + "message": "Arhiv", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "Arhiviraj", "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Odstrani iz arhiva" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Elementi v arhivu" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "Ni elementov v arhivu" }, "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -595,10 +592,10 @@ "message": "Unarchive and save" }, "upgradeToUseArchive": { - "message": "A premium membership is required to use Archive." + "message": "Za uporabo Arhiva je potrebno premium članstvo." }, "itemRestored": { - "message": "Item has been restored" + "message": "Vnos je bil obnovljen" }, "edit": { "message": "Uredi" @@ -607,13 +604,13 @@ "message": "Pogled" }, "viewAll": { - "message": "View all" + "message": "Poglej vse" }, "showAll": { - "message": "Show all" + "message": "Prikaži vse" }, "viewLess": { - "message": "View less" + "message": "Poglej manj" }, "viewLogin": { "message": "View login" @@ -643,10 +640,10 @@ "message": "Unfavorite" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Element dodan med priljubljene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Element odstranen iz priljubljenih" }, "notes": { "message": "Opombe" @@ -712,7 +709,7 @@ "message": "Vault timeout" }, "otherOptions": { - "message": "Other options" + "message": "Ostale možnosti" }, "rateExtension": { "message": "Ocenite to razširitev" @@ -721,25 +718,25 @@ "message": "Vaš brskalnik ne podpira enostavnega kopiranja na odložišče. Prosimo, kopirajte ročno." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Potrdite vašo identiteto" }, "weDontRecognizeThisDevice": { - "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." + "message": "Te naprave ne prepoznamo. Vnesite kodo, ki je bila poslana na vaš e-poštni naslov, da potrdite vašo identiteto." }, "continueLoggingIn": { - "message": "Continue logging in" + "message": "Nadaljujte s prijavo" }, "yourVaultIsLocked": { "message": "Vaš trezor je zaklenjen. Za nadaljevanje potrdite svojo identiteto." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Vaš trezor je zaklenjen" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Vaš račun je zaklenjen" }, "or": { - "message": "or" + "message": "ali" }, "unlock": { "message": "Odkleni" @@ -761,7 +758,7 @@ "message": "Napačno glavno geslo" }, "invalidMasterPasswordConfirmEmailAndHost": { - "message": "Invalid master password. Confirm your email is correct and your account was created on $HOST$.", + "message": "Neveljavno glavno geslo. Preverite, da je vaš e-poštni naslov pravilen, ter da je bil vaš račun ustvarjen na $HOST$.", "placeholders": { "host": { "content": "$1", @@ -779,7 +776,7 @@ "message": "Zakleni zdaj" }, "lockAll": { - "message": "Lock all" + "message": "Zakleni vse" }, "immediately": { "message": "Takoj" @@ -836,13 +833,13 @@ "message": "Confirm master password" }, "masterPassword": { - "message": "Master password" + "message": "Glavno geslo" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Če pozabite glavno geslo, ga ne bo mogoče obnoviti!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Namig za glavno geslo" }, "errorOccurred": { "message": "Prišlo je do napake" @@ -879,7 +876,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Prijavljeni ste!" }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Neveljavna koda za preverjanje" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ kopirana", "description": "Value has been copied to the clipboard.", @@ -973,7 +973,7 @@ "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Pretečena povezava" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -997,7 +997,7 @@ "message": "Anyone with a password set by you" }, "location": { - "message": "Location" + "message": "Lokacija" }, "unexpectedError": { "message": "Prišlo je do nepričakovane napake." @@ -1012,10 +1012,10 @@ "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Zavarujte vaš račun tako, da nastavite dvostopenjsko prijavo v Bitwarden spletni aplikaciji." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Nadaljuj v spletno aplikacijo?" }, "editedFolder": { "message": "Mapa shranjena" @@ -1058,7 +1058,7 @@ "message": "Nov URI" }, "addDomain": { - "message": "Add domain", + "message": "Dodaj domeno", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6065,7 +6107,7 @@ "message": "Contact your admin to regain access." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "Zapusti $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6074,10 +6116,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Kako uporabljam svoj trezor?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Prenesi elemente v $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6095,19 +6137,19 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Sprejmi prenos" }, "declineAndLeave": { "message": "Decline and leave" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "Zakaj se mi to prikazuje?" }, "items": { - "message": "Items" + "message": "Elementi" }, "searchResults": { - "message": "Search results" + "message": "Rezultati iskanja" }, "resizeSideNavigation": { "message": "Resize side navigation" @@ -6124,11 +6166,14 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "uporabnik@bitwarden.com , uporabnik@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Prenesi Bitwarden aplikacije" }, "emailProtected": { "message": "Email protected" @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0ad71788514..b375ca5d536 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ копиран(а/о)", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Илустрација листе пријаве које су ризичне." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Брзо генеришите снажну, јединствену лозинку са Bitwarden менијем аутопуњења за коришћење на ризичном сајту.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send линк је копиран", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 08cec673d27..eb5a7fef645 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" - }, - "itemUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ har kopierats", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration av en lista över inloggningar som är i riskzonen." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Skapa snabbt ett starkt, unikt lösenord med Bitwardens autofyllmeny på riskwebbplatsen.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ timmar", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med länken för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Kopiera och dela denna Send-länk. Denna Send kommer att vara tillgänglig för alla med den länk och lösenord du angav för nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Kopiera och dela denna Send-länk. Den kan visas av personer som du har angivet nästa $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Skicka länk kopierad", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 374c0968d2c..72ad809e723 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "காப்பகப்படுத்தப்பட்ட உருப்படிகள் இங்கே தோன்றும், மேலும் அவை பொதுவான தேடல் முடிவுகள் மற்றும் தானியங்குநிரப்பு பரிந்துரைகளிலிருந்து விலக்கப்படும்." }, - "itemWasSentToArchive": { - "message": "ஆவணம் காப்பகத்திற்கு அனுப்பப்பட்டது" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "காப்பகம் மீட்டெடுக்கப்பட்டது" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "உருப்படியைக் காப்பகப்படுத்து" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ நகலெடுக்கப்பட்டது", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ஆபத்தில் உள்ள உள்நுழைவுகளின் பட்டியலின் விளக்கம்." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "ஆபத்தில் உள்ள தளத்தில் உள்ள Bitwarden தானாக நிரப்பு மெனுவுடன் ஒரு வலுவான, தனிப்பட்ட கடவுச்சொல்லை விரைவாக உருவாக்குங்கள்.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "அனுப்பு இணைப்பு நகலெடுக்கப்பட்டது", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 336e8783b75..51ca51960d7 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ copied", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Illustration of a list of logins that are at-risk." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 5af1c742f45..82878eb3b52 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "รายการที่จัดเก็บถาวรจะปรากฏที่นี่ และจะไม่ถูกรวมในผลการค้นหาทั่วไปหรือคำแนะนำการป้อนอัตโนมัติ" }, - "itemWasSentToArchive": { - "message": "ย้ายรายการไปที่จัดเก็บถาวรแล้ว" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "เลิกจัดเก็บถาวรรายการแล้ว" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "จัดเก็บรายการถาวร" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "รหัสยืนยันไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "คัดลอก $VALUE$ แล้ว", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "ภาพประกอบรายการข้อมูลเข้าสู่ระบบที่มีความเสี่ยง" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "สร้างรหัสผ่านที่รัดกุมและไม่ซ้ำกันอย่างรวดเร็วด้วยเมนูป้อนอัตโนมัติของ Bitwarden บนเว็บไซต์ที่มีความเสี่ยง", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "คัดลอกลิงก์ Send แล้ว", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 33f600fb7a7..b3d1d46c9a5 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçlarından ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { - "message": "Kayıt arşivden çıkarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "valueCopied": { "message": "$VALUE$ kopyalandı", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Risk altındaki hesap listesinin illüstrasyonu." }, + "welcomeDialogGraphicAlt": { + "message": "Bitwarden kasa sayfası düzeninin illüstrasyonu." + }, "generatePasswordSlideDesc": { "message": "Riskli sitede Bitwarden otomatik doldurma menüsünü kullanarak hızlıca güçlü ve benzersiz bir parola oluştur.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Bu Send'e önümüzdeki $TIME$ boyunca bağlantıya ve parolaya sahip herkes ulaşabilecektir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Bu Send bağlantısını kopyalayıp paylaşın. Belirlediğiniz kişiler bağlantıyı önümüzdeki $TIME$ boyunca kullanabilir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send bağlantısı kopyalandı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b703cfeefce..143dc8037fd 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Запис архівовано" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Запис розархівовано" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Недійсна е-пошта або код підтвердження" + }, "valueCopied": { "message": "$VALUE$ скопійовано", "description": "Value has been copied to the clipboard.", @@ -1144,7 +1144,7 @@ "message": "Натисніть на запис у режимі перегляду сховища для автозаповнення" }, "clickToAutofill": { - "message": "Натисніть запис у пропозиціях для автозаповнення" + "message": "Натиснути запис у пропозиціях для автозаповнення" }, "clearClipboard": { "message": "Очистити буфер обміну", @@ -2055,7 +2055,7 @@ "message": "Е-пошта" }, "emails": { - "message": "Е-пошти" + "message": "Адреси е-пошти" }, "phone": { "message": "Телефон" @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Ілюстрація списку ризикованих записів." }, + "welcomeDialogGraphicAlt": { + "message": "Ілюстрація макету сторінки сховища Bitwarden." + }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ годин", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням усім протягом $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Відправлення буде доступне за посиланням і встановленим вами паролем усім протягом $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Скопіюйте посилання на це відправлення і поділіться ним. Його зможуть переглядати зазначені вами користувачі протягом $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Посилання на відправлення скопійовано", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -3356,7 +3398,7 @@ "message": "Не вдалося розблокувати за допомогою ключа доступу. Повторіть спробу або скористайтеся іншим способом розблокування." }, "noPrfCredentialsAvailable": { - "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку увійдіть з ключем доступу." + "message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку ввійдіть з ключем доступу." }, "decryptionError": { "message": "Помилка розшифрування" @@ -4692,7 +4734,7 @@ "message": "Запропоновані записи" }, "autofillSuggestionsTip": { - "message": "Зберегти дані входу цього сайту для автозаповнення" + "message": "Збережіть дані входу цього сайту для автозаповнення" }, "yourVaultIsEmpty": { "message": "Ваше сховище порожнє" @@ -4734,7 +4776,7 @@ } }, "moreOptionsLabelNoPlaceholder": { - "message": "Більше опцій" + "message": "Інші варіанти" }, "moreOptionsTitle": { "message": "Інші можливості – $ITEMNAME$", @@ -5668,7 +5710,7 @@ "message": "Дуже широке" }, "narrow": { - "message": "Вузький" + "message": "Вузьке" }, "sshKeyWrongPassword": { "message": "Ви ввели неправильний пароль." @@ -5964,7 +6006,7 @@ "message": "Номер картки" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Помилка: неможливо розшифрувати" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "Ваша організація більше не використовує головні паролі для входу в Bitwarden. Щоб продовжити, підтвердіть організацію та домен." @@ -6116,25 +6158,31 @@ "message": "Хто може переглядати" }, "specificPeople": { - "message": "Певні люди" + "message": "Певні користувачі" }, "emailVerificationDesc": { - "message": "Після того, як ви поділитеся цим посиланням на відправлення, особам необхідно буде підтвердити свої е-пошти за допомогою коду, щоб переглянути це відправлення." + "message": "Після того, як ви поділитеся посиланням на це відправлення, користувачі мають підтвердити свою е-пошту за допомогою коду, щоб переглянути його." }, "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Для підтвердження адреси електронної пошти потрібна щонайменше одна адреса. Щоб вилучити всі адреси електронної пошти, змініть тип доступу вище." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, "downloadBitwardenApps": { - "message": "Download Bitwarden apps" + "message": "Завантажити програми Bitwarden" }, "emailProtected": { "message": "Е-пошту захищено" }, "sendPasswordHelperText": { - "message": "Особам необхідно ввести пароль для перегляду цього відправлення", + "message": "Користувачі мають ввести пароль для перегляду цього відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "Не вдалося перевірити користувача." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 0082ee1ece7..ebd3a2500aa 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" - }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "Đã sao chép $VALUE$", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "Minh họa danh sách các tài khoản đăng nhập có rủi ro." }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "Tạo nhanh một mật khẩu mạnh, duy nhất bằng menu tự động điền của Bitwarden trên trang web có nguy cơ.", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Đã sao chép liên kết Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 860a8c09f27..c27d1a8bb24 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -573,13 +573,10 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "项目已归档" }, - "itemWasUnarchived": { - "message": "项目已取消归档" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "项目已取消归档" }, "archiveItem": { @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "无效的电子邮箱或验证码" + }, "valueCopied": { "message": "$VALUE$ 已复制", "description": "Value has been copied to the clipboard.", @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "存在风险的登录列表示意图。" }, + "welcomeDialogGraphicAlt": { + "message": "Bitwarden 密码库页面布局示意图。" + }, "generatePasswordSlideDesc": { "message": "在存在风险的网站上,使用 Bitwarden 自动填充菜单快速生成强大且唯一的密码。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小时", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,拥有此链接以及您设置的密码的任何人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "复制并分享此 Send 链接。在接下来的 $TIME$ 内,您指定的人员可查看此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "Send 链接已复制", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "用户验证失败。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 3f387d935d4..d23106948ad 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -228,7 +228,7 @@ "message": "複製自訂欄位名稱" }, "noMatchingLogins": { - "message": "沒有符合的登入資料" + "message": "沒有相符的登入項目" }, "noCards": { "message": "沒有付款卡" @@ -252,7 +252,7 @@ "message": "登入您的密碼庫" }, "autoFillInfo": { - "message": "沒有可以在目前瀏覽器分頁自動填入的登入資訊。" + "message": "目前瀏覽器分頁沒有可自動填入的登入項目。" }, "addLogin": { "message": "新增登入資料" @@ -453,10 +453,10 @@ "description": "Short for 'credential generator'." }, "passGenInfo": { - "message": "為您的登入資料自動產生高強度且唯一的密碼。" + "message": "為您的登入項目自動產生高強度且唯一的密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden 網頁應用程式" + "message": "Bitwarden Web 應用程式" }, "select": { "message": "選擇" @@ -468,16 +468,16 @@ "message": "產生密碼短語" }, "passwordGenerated": { - "message": "已產生密碼" + "message": "密碼已產生" }, "passphraseGenerated": { - "message": "已產生密碼" + "message": "密碼短語已產生" }, "usernameGenerated": { - "message": "已產生使用者名稱" + "message": "使用者名稱已產生" }, "emailGenerated": { - "message": "已產生電子郵件" + "message": "電子郵件已產生" }, "regeneratePassword": { "message": "重新產生密碼" @@ -573,14 +573,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "項目已封存" }, - "itemWasUnarchived": { - "message": "已取消封存項目" - }, - "itemUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "項目已取消封存" }, "archiveItem": { "message": "封存項目" @@ -598,7 +595,7 @@ "message": "需要進階版會員才能使用封存功能。" }, "itemRestored": { - "message": "已還原項目" + "message": "項目已還原" }, "edit": { "message": "編輯" @@ -899,6 +896,9 @@ "invalidVerificationCode": { "message": "驗證碼無效" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "valueCopied": { "message": "$VALUE$ 已複製", "description": "Value has been copied to the clipboard.", @@ -1215,7 +1215,7 @@ "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "更新登入資料", + "message": "更新登入項目", "description": "Button text for updating an existing login entry." }, "unlockToSave": { @@ -1223,7 +1223,7 @@ "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "儲存登入資料", + "message": "儲存登入項目", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { @@ -1231,7 +1231,7 @@ "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "登入資訊已儲存", + "message": "登入項目已儲存", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { @@ -1305,7 +1305,7 @@ "message": "解鎖" }, "additionalOptions": { - "message": "額外選項" + "message": "其他選項" }, "enableContextMenuItem": { "message": "顯示內容選單選項" @@ -1567,7 +1567,7 @@ "message": "提供密碼健全性、帳戶健康狀態及資料外洩報告,確保您的密碼庫安全。" }, "ppremiumSignUpTotp": { - "message": "為密碼庫中的登入資料產生 TOTP 驗證碼(2FA)。" + "message": "為密碼庫中的登入項目產生 TOTP 驗證碼(2FA)。" }, "ppremiumSignUpSupport": { "message": "優先客戶支援。" @@ -1801,7 +1801,7 @@ "message": "Bitwarden 如何保護您的資料免於網路釣魚攻擊?" }, "currentWebsite": { - "message": "目網站" + "message": "目前網站" }, "autofillAndAddWebsite": { "message": "自動填充並新增此網站" @@ -2091,7 +2091,7 @@ "message": "登入資料" }, "typeLogins": { - "message": "登入資料" + "message": "登入項目" }, "typeSecureNote": { "message": "安全筆記" @@ -2227,7 +2227,7 @@ "message": "身分" }, "logins": { - "message": "登入資料" + "message": "登入項目" }, "secureNotes": { "message": "安全筆記" @@ -2513,7 +2513,7 @@ "message": "項目已自動填入並且已儲存統一資源標識符(URI)" }, "autoFillSuccess": { - "message": "項目已自動填入 " + "message": "項目已自動填入" }, "insecurePageWarning": { "message": "警告:此為不安全的 HTTP 頁面,您送出的任何資訊都可能被他人查看並修改。此登入資料原本儲存在安全的(HTTPS)頁面上。" @@ -2773,10 +2773,10 @@ } }, "atRiskPassword": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswords": { - "message": "具有風險的密碼" + "message": "有風險的密碼" }, "atRiskPasswordDescSingleOrg": { "message": "$ORGANIZATION$ 要求你變更一組有風險的密碼。", @@ -2848,7 +2848,7 @@ "message": "更新你的設定,以便能快速自動填入密碼並產生新密碼" }, "reviewAtRiskLogins": { - "message": "檢視有風險的登入資訊" + "message": "檢視有風險的登入項目" }, "reviewAtRiskPasswords": { "message": "檢視有風險的密碼" @@ -2860,6 +2860,9 @@ "reviewAtRiskLoginSlideImgAltPeriod": { "message": "有風險登入清單的示意圖。" }, + "welcomeDialogGraphicAlt": { + "message": "Illustration of the layout of the Bitwarden vault page." + }, "generatePasswordSlideDesc": { "message": "在有風險的網站上,透過 Bitwarden 自動填入選單快速產生強且唯一的密碼。", "description": "Description of the generate password slide on the at-risk password page carousel" @@ -3083,6 +3086,45 @@ } } }, + "durationTimeHours": { + "message": "$HOURS$ 小時", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "sendCreatedDescriptionV2": { + "message": "複製並分享此 Send 連結。任何擁有此連結的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionPassword": { + "message": "複製並分享此 Send 連結。任何擁有此連結與您所設定密碼的人,都可在接下來的 $TIME$ 內存取該 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, + "sendCreatedDescriptionEmail": { + "message": "複製並分享此 Send 連結。在接下來的 $TIME$ 內,只有您指定的人可以檢視。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days, 1 hour, 1 day" + } + } + }, "sendLinkCopied": { "message": "已複製 Send 連結", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5749,7 +5791,7 @@ "message": "設定生物辨識解鎖及自動填入,不需要輸入任何字元就可以登入。" }, "secureUser": { - "message": "升級您的登入體驗" + "message": "讓您的登入項目更升級" }, "secureUserBody": { "message": "使用密碼產生器來建立及儲存高強度、唯一的密碼,來保護您所有的帳號。" @@ -5964,7 +6006,7 @@ "message": "付款卡號碼" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "錯誤:無法解密" }, "removeMasterPasswordForOrgUserKeyConnector": { "message": "您的組織已不再使用主密碼登入 Bitwarden。若要繼續,請驗證組織與網域。" @@ -6124,6 +6166,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -6136,5 +6181,8 @@ "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html index 0a9e2a1dd9d..2eeaaf35f53 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html @@ -58,19 +58,19 @@ [disabled]="currentAccount.status === lockedStatus || !activeUserCanLock" [title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''" > - + {{ "lockNow" | i18n }} diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index ae7f66a9018..53d488192ba 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -18,6 +18,7 @@ import { AvatarModule, ButtonModule, DialogService, + IconModule, ItemModule, SectionComponent, SectionHeaderComponent, @@ -42,6 +43,7 @@ import { AccountSwitcherService } from "./services/account-switcher.service"; ButtonModule, ItemModule, AvatarModule, + IconModule, PopupPageComponent, PopupHeaderComponent, PopOutComponent, diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index 35f4477fa1e..eefda3aecb4 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -31,13 +31,13 @@ - + diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index edfad2a54b3..dbc31a1f011 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -8,7 +8,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AvatarModule, ItemModule } from "@bitwarden/components"; +import { AvatarModule, IconModule, ItemModule, type BitwardenIcon } from "@bitwarden/components"; import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; @@ -18,7 +18,7 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi @Component({ selector: "auth-account", templateUrl: "account.component.html", - imports: [CommonModule, JslibModule, AvatarModule, ItemModule], + imports: [CommonModule, JslibModule, AvatarModule, IconModule, ItemModule], }) export class AccountComponent { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals @@ -60,7 +60,7 @@ export class AccountComponent { this.loading.emit(false); } - get status() { + get status(): { text: string; icon: BitwardenIcon } { if (this.account.isActive) { return { text: this.i18nService.t("active"), icon: "bwi-check-circle" }; } diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index bb6b141c6c5..366f5b82790 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -123,7 +123,7 @@ @@ -154,13 +154,13 @@ diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 1789feebe4e..b3bd9b842f7 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -56,6 +56,7 @@ import { DialogService, FormFieldModule, IconButtonModule, + IconModule, ItemModule, LinkModule, SectionComponent, @@ -98,6 +99,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; FormsModule, ReactiveFormsModule, IconButtonModule, + IconModule, ItemModule, JslibModule, LinkModule, diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 95d4111987b..1bd1ae5513b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -28,6 +28,7 @@ import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks"; @@ -81,6 +82,8 @@ describe("NotificationBackground", () => { const configService = mock(); const accountService = mock(); const organizationService = mock(); + const fido2Background = mock(); + fido2Background.isCredentialRequestInProgress.mockReturnValue(false); const userId = "testId" as UserId; const activeAccountSubject = new BehaviorSubject({ @@ -115,6 +118,7 @@ describe("NotificationBackground", () => { userNotificationSettingsService, taskService, messagingService, + fido2Background, ); }); @@ -759,7 +763,6 @@ describe("NotificationBackground", () => { notificationBackground as any, "getEnableChangedPasswordPrompt", ); - pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -822,6 +825,22 @@ describe("NotificationBackground", () => { expectSkippedCheckingNotification(); }); + it("skips checking if a notification should trigger if a fido2 credential request is in progress for the tab", async () => { + const formEntryData: ModifyLoginCipherFormData = { + newPassword: "", + password: "", + uri: mockFormURI, + username: "ADent", + }; + + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + fido2Background.isCredentialRequestInProgress.mockReturnValueOnce(true); + + await notificationBackground.triggerCipherNotification(formEntryData, tab); + + expectSkippedCheckingNotification(); + }); + it("skips checking if a notification should trigger if the user has disabled both the new login and update password notification", async () => { const formEntryData: ModifyLoginCipherFormData = { newPassword: "Bab3lPhs5h", diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 3713cd7c4c2..64c52701e21 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -61,6 +61,7 @@ import { } from "../content/components/cipher/types"; import { CollectionView } from "../content/components/common-types"; import { NotificationType } from "../enums/notification-type.enum"; +import { Fido2Background } from "../fido2/background/abstractions/fido2.background"; import { AutofillService } from "../services/abstractions/autofill.service"; import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service"; @@ -165,6 +166,7 @@ export default class NotificationBackground { private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private taskService: TaskService, protected messagingService: MessagingService, + private fido2Background: Fido2Background, ) {} init() { @@ -665,6 +667,11 @@ export default class NotificationBackground { return false; } + // If there is an active passkey prompt, exit early + if (this.fido2Background.isCredentialRequestInProgress(tab.id)) { + return false; + } + // If no cipher add/update notifications are enabled, we can exit early const changePasswordNotificationIsEnabled = await this.getEnableChangedPasswordPrompt(); const newLoginNotificationIsEnabled = await this.getEnableAddedLoginPrompt(); diff --git a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts index 6ad069ad56e..c5346d61566 100644 --- a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts @@ -45,6 +45,8 @@ type Fido2BackgroundExtensionMessageHandlers = { interface Fido2Background { init(): void; + isCredentialRequestInProgress(tabId: number): boolean; + isPasskeySettingEnabled(): Promise; } export { diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 752851b3d37..6ead7416b96 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -256,6 +256,84 @@ describe("Fido2Background", () => { }); }); + describe("isCredentialRequestInProgress", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("returns false when no credential request is active", () => { + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns true while a register credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.createCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns true while a get credential request is in progress", async () => { + let duringRequestResult: boolean; + fido2ClientService.assertCredential.mockImplementation(async () => { + duringRequestResult = fido2Background.isCredentialRequestInProgress(tabMock.id); + return mock(); + }); + + const message = mock({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(duringRequestResult).toBe(true); + }); + + it("returns false after a credential request completes", async () => { + fido2ClientService.createCredential.mockResolvedValue(mock()); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + + it("returns false after a credential request errors", async () => { + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendMockExtensionMessage(message, senderMock); + await flushPromises(); + + expect(fido2Background.isCredentialRequestInProgress(tabMock.id)).toBe(false); + }); + }); + describe("extension message handlers", () => { beforeEach(() => { fido2Background.init(); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 22ee4a1822d..495b0d85f0b 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -35,6 +35,7 @@ export class Fido2Background implements Fido2BackgroundInterface { private currentAuthStatus$: Subscription; private abortManager = new AbortManager(); private fido2ContentScriptPortsSet = new Set(); + private activeCredentialRequests = new Set(); private registeredContentScripts: browser.contentScripts.RegisteredContentScript; private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { runAt: "document_start", @@ -61,6 +62,16 @@ export class Fido2Background implements Fido2BackgroundInterface { private authService: AuthService, ) {} + /** + * Checks if a FIDO2 credential request (registration or assertion) + * is currently in progress for the given tab. + * + * @param tabId - The tab id to check. + */ + isCredentialRequestInProgress(tabId: number): boolean { + return this.activeCredentialRequests.has(tabId); + } + /** * Initializes the FIDO2 background service. Sets up the extension message * and port listeners. Subscribes to the enablePasskeys$ observable to @@ -307,20 +318,25 @@ export class Fido2Background implements Fido2BackgroundInterface { abortController: AbortController, ) => Promise, ) => { - return await this.abortManager.runWithAbortController(requestId, async (abortController) => { - try { - return await callback(data, tab, abortController); - } finally { - await BrowserApi.focusTab(tab.id); - await BrowserApi.focusWindow(tab.windowId); - } - }); + this.activeCredentialRequests.add(tab.id); + try { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + } finally { + this.activeCredentialRequests.delete(tab.id); + } }; /** * Checks if the enablePasskeys setting is enabled. */ - private async isPasskeySettingEnabled() { + async isPasskeySettingEnabled() { return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 19c1dbc8790..11dc170db16 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -363,6 +363,9 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ), ); + // Defensive measure in case an existing notification appeared before the passkey popout + await BrowserApi.tabSendMessageData(this.tab, "closeNotificationBar"); + const popoutId = await openFido2Popout(this.tab, { sessionId: this.sessionId, fallbackSupported: this.fallbackSupported, diff --git a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts index 07ffa553b07..795e35e1e80 100644 --- a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts +++ b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts @@ -27,9 +27,11 @@ export class WebauthnUtils { residentKey: keyOptions.authenticatorSelection?.residentKey, userVerification: keyOptions.authenticatorSelection?.userVerification, }, - challenge: Fido2Utils.bufferToString(keyOptions.challenge), + challenge: Fido2Utils.arrayToString( + Fido2Utils.bufferSourceToUint8Array(keyOptions.challenge), + ), excludeCredentials: keyOptions.excludeCredentials?.map((credential) => ({ - id: Fido2Utils.bufferToString(credential.id), + id: Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(credential.id)), transports: credential.transports, type: credential.type, })), @@ -48,7 +50,7 @@ export class WebauthnUtils { name: keyOptions.rp.name, }, user: { - id: Fido2Utils.bufferToString(keyOptions.user.id), + id: Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(keyOptions.user.id)), displayName: keyOptions.user.displayName, name: keyOptions.user.name, }, @@ -60,19 +62,19 @@ export class WebauthnUtils { static mapCredentialRegistrationResult(result: CreateCredentialResult): PublicKeyCredential { const credential = { id: result.credentialId, - rawId: Fido2Utils.stringToBuffer(result.credentialId), + rawId: Fido2Utils.stringToArray(result.credentialId).buffer, type: "public-key", authenticatorAttachment: "platform", response: { - clientDataJSON: Fido2Utils.stringToBuffer(result.clientDataJSON), - attestationObject: Fido2Utils.stringToBuffer(result.attestationObject), + clientDataJSON: Fido2Utils.stringToArray(result.clientDataJSON).buffer, + attestationObject: Fido2Utils.stringToArray(result.attestationObject).buffer, getAuthenticatorData(): ArrayBuffer { - return Fido2Utils.stringToBuffer(result.authData); + return Fido2Utils.stringToArray(result.authData).buffer; }, getPublicKey(): ArrayBuffer { - return Fido2Utils.stringToBuffer(result.publicKey); + return Fido2Utils.stringToArray(result.publicKey).buffer; }, getPublicKeyAlgorithm(): number { @@ -110,8 +112,12 @@ export class WebauthnUtils { return { allowedCredentialIds: - keyOptions.allowCredentials?.map((c) => Fido2Utils.bufferToString(c.id)) ?? [], - challenge: Fido2Utils.bufferToString(keyOptions.challenge), + keyOptions.allowCredentials?.map((c) => + Fido2Utils.arrayToString(Fido2Utils.bufferSourceToUint8Array(c.id)), + ) ?? [], + challenge: Fido2Utils.arrayToString( + Fido2Utils.bufferSourceToUint8Array(keyOptions.challenge), + ), rpId: keyOptions.rpId, userVerification: keyOptions.userVerification, timeout: keyOptions.timeout, @@ -123,13 +129,13 @@ export class WebauthnUtils { static mapCredentialAssertResult(result: AssertCredentialResult): PublicKeyCredential { const credential = { id: result.credentialId, - rawId: Fido2Utils.stringToBuffer(result.credentialId), + rawId: Fido2Utils.stringToArray(result.credentialId).buffer, type: "public-key", response: { - authenticatorData: Fido2Utils.stringToBuffer(result.authenticatorData), - clientDataJSON: Fido2Utils.stringToBuffer(result.clientDataJSON), - signature: Fido2Utils.stringToBuffer(result.signature), - userHandle: Fido2Utils.stringToBuffer(result.userHandle), + authenticatorData: Fido2Utils.stringToArray(result.authenticatorData).buffer, + clientDataJSON: Fido2Utils.stringToArray(result.clientDataJSON).buffer, + signature: Fido2Utils.stringToArray(result.signature).buffer, + userHandle: Fido2Utils.stringToArray(result.userHandle).buffer, } as AuthenticatorAssertionResponse, getClientExtensionResults: () => ({}), authenticatorAttachment: "platform", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 585942d7537..a45b371a9da 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -14,7 +14,14 @@ import { timeout, } from "rxjs"; -import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; +import { + CollectionService, + DefaultCollectionService, + DefaultOrganizationUserApiService, + DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserService, +} from "@bitwarden/admin-console/common"; import { AuthRequestApiServiceAbstraction, AuthRequestService, @@ -27,6 +34,10 @@ import { LogoutReason, UserDecryptionOptionsService, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -100,7 +111,9 @@ import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/ import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service"; +import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { DefaultMasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/services/default-master-password-unlock.service"; import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service"; import { PinStateService } from "@bitwarden/common/key-management/pin/pin-state.service.implementation"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; @@ -362,6 +375,7 @@ export default class MainBackground { keyService: KeyServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; masterPasswordService: InternalMasterPasswordServiceAbstraction; + masterPasswordUnlockService: MasterPasswordUnlockService; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -487,6 +501,9 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: CipherView = null; + organizationUserService: OrganizationUserService; + organizationUserApiService: OrganizationUserApiService; + autoConfirmService: AutomaticUserConfirmationService; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; @@ -718,6 +735,12 @@ export default class MainBackground { this.accountCryptographicStateService, ); + this.masterPasswordUnlockService = new DefaultMasterPasswordUnlockService( + this.masterPasswordService, + this.keyService, + this.logService, + ); + const pinStateService = new PinStateService(this.stateProvider); this.appIdService = new AppIdService(this.storageService, this.logService); @@ -763,6 +786,15 @@ export default class MainBackground { { createRequest: (url, request) => new Request(url, request) }, ); + this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); + this.organizationUserService = new DefaultOrganizationUserService( + this.keyService, + this.encryptService, + this.organizationUserApiService, + this.accountService, + this.i18nService, + ); + this.hibpApiService = new HibpApiService(this.apiService); this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( @@ -804,6 +836,16 @@ export default class MainBackground { this.authService, ); + this.autoConfirmService = new DefaultAutomaticUserConfirmationService( + this.configService, + this.apiService, + this.organizationUserService, + this.stateProvider, + this.organizationService, + this.organizationUserApiService, + this.policyService, + ); + const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); @@ -815,7 +857,7 @@ export default class MainBackground { this.accountService, this.kdfConfigService, this.keyService, - this.securityStateService, + this.accountCryptographicStateService, this.apiService, this.stateProvider, this.configService, @@ -915,8 +957,6 @@ export default class MainBackground { this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.stateProvider, - this.platformUtilsService, - this.apiService, ); this.restrictedItemTypesService = new RestrictedItemTypesService( @@ -991,6 +1031,7 @@ export default class MainBackground { this.pinService, this.kdfConfigService, this.biometricsService, + this.masterPasswordUnlockService, ); this.vaultSettingsService = new VaultSettingsService( @@ -1219,6 +1260,7 @@ export default class MainBackground { this.authRequestAnsweringService, this.configService, this.policyService, + this.autoConfirmService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -1375,6 +1417,7 @@ export default class MainBackground { this.userNotificationSettingsService, this.taskService, this.messagingService, + this.fido2Background, ); this.overlayNotificationsBackground = new OverlayNotificationsBackground( diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index ce5311f848a..54a10e34b12 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2026.1.0", + "version": "2026.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 9cb77aa3040..206a300236c 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2026.1.0", + "version": "2026.2.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index f7561b2b50b..d8a8fe52570 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +import { LogService } from "@bitwarden/logging"; + import { BrowserApi } from "./browser-api"; type ChromeSettingsGet = chrome.types.ChromeSetting["get"]; @@ -29,6 +31,104 @@ describe("BrowserApi", () => { }); }); + describe("senderIsInternal", () => { + const EXTENSION_ORIGIN = "chrome-extension://id"; + + beforeEach(() => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(`${EXTENSION_ORIGIN}/`); + }); + + it("returns false when sender is undefined", () => { + const result = BrowserApi.senderIsInternal(undefined); + + expect(result).toBe(false); + }); + + it("returns false when sender has no origin", () => { + const result = BrowserApi.senderIsInternal({ id: "abc" } as any); + + expect(result).toBe(false); + }); + + it("returns false when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(false); + }); + + it.each([ + ["an external origin", "https://evil.com"], + ["a subdomain of the extension origin", "chrome-extension://id.evil.com"], + ["a file: URL (opaque origin)", "file:///home/user/page.html"], + ["a data: URL (opaque origin)", "data:text/html,

hi

"], + ])("returns false when sender origin is %s", (_, senderOrigin) => { + const result = BrowserApi.senderIsInternal({ origin: senderOrigin }); + + expect(result).toBe(false); + }); + + it("returns false when sender is from a non-top-level frame", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }); + + expect(result).toBe(false); + }); + + it("returns true when sender origin matches and no frameId is present (popup)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }); + + expect(result).toBe(true); + }); + + it("returns true when sender origin matches and frameId is 0 (top-level frame)", () => { + const result = BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 0 }); + + expect(result).toBe(true); + }); + + it("calls logger.warning when sender has no origin", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({} as any, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("no origin")); + }); + + it("calls logger.warning when the extension URL cannot be determined", () => { + jest.spyOn(BrowserApi, "getRuntimeURL").mockReturnValue(""); + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("extension URL")); + }); + + it("calls logger.warning when origin does not match", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: "https://evil.com" }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("does not match")); + }); + + it("calls logger.warning when sender is from a non-top-level frame", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN, frameId: 5 }, logger); + + expect(logger.warning).toHaveBeenCalledWith(expect.stringContaining("top-level frame")); + }); + + it("calls logger.info when sender is confirmed internal", () => { + const logger = mock(); + + BrowserApi.senderIsInternal({ origin: EXTENSION_ORIGIN }, logger); + + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("internal")); + }); + }); + describe("getWindow", () => { it("will get the current window if a window id is not provided", () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index feefd527636..1b0f7639d1d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -6,7 +6,7 @@ import { BrowserClientVendors } from "@bitwarden/common/autofill/constants"; import { BrowserClientVendor } from "@bitwarden/common/autofill/types"; import { DeviceType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/logging"; -import { isBrowserSafariApi } from "@bitwarden/platform"; +import { isBrowserSafariApi, urlOriginsMatch } from "@bitwarden/platform"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; @@ -34,12 +34,20 @@ export class BrowserApi { } /** - * Helper method that attempts to distinguish whether a message sender is internal to the extension or not. + * Returns `true` if the message sender appears to originate from within this extension. * - * Currently this is done through source origin matching, and frameId checking (only top-level frames are internal). - * @param sender a message sender - * @param logger an optional logger to log validation results - * @returns whether or not the sender appears to be internal to the extension + * Returns `false` when: + * - `sender` is absent or has no `origin` property + * - The extension's own URL cannot be determined at runtime + * - The sender's origin does not match the extension's origin (compared by scheme, host, and port; + * senders without a host such as `file:` or `data:` URLs are always rejected) + * - The message comes from a sub-frame rather than the top-level frame + * + * Note: this is a best-effort check that relies on the browser correctly populating `sender.origin`. + * + * @param sender - The message sender to validate. `undefined` or a sender without `origin` returns `false`. + * @param logger - Optional logger; rejections are reported at `warning` level, acceptance at `info`. + * @returns `true` if the sender appears to be internal to the extension; `false` otherwise. */ static senderIsInternal( sender: chrome.runtime.MessageSender | undefined, @@ -49,28 +57,22 @@ export class BrowserApi { logger?.warning("[BrowserApi] Message sender has no origin"); return false; } - const extensionUrl = - (typeof chrome !== "undefined" && chrome.runtime?.getURL("")) || - (typeof browser !== "undefined" && browser.runtime?.getURL("")) || - ""; + // Empty path yields the extension's base URL; coalesce to empty string so the guard below fires on a missing runtime. + const extensionUrl = BrowserApi.getRuntimeURL("") ?? ""; if (!extensionUrl) { logger?.warning("[BrowserApi] Unable to determine extension URL"); return false; } - // Normalize both URLs by removing trailing slashes - const normalizedOrigin = sender.origin.replace(/\/$/, "").toLowerCase(); - const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "").toLowerCase(); - - if (!normalizedOrigin.startsWith(normalizedExtensionUrl)) { + if (!urlOriginsMatch(extensionUrl, sender.origin)) { logger?.warning( - `[BrowserApi] Message sender origin (${normalizedOrigin}) does not match extension URL (${normalizedExtensionUrl})`, + `[BrowserApi] Message sender origin (${sender.origin}) does not match extension URL (${extensionUrl})`, ); return false; } - // We only send messages from the top-level frame, but frameId is only set if tab is set, which for popups it is not. + // frameId is absent for popups, so use an 'in' check rather than direct comparison. if ("frameId" in sender && sender.frameId !== 0) { logger?.warning("[BrowserApi] Message sender is not from the top-level frame"); return false; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4e14d1171fd..01d713742a5 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -42,7 +42,7 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; -import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm"; +import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm/angular"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, @@ -356,7 +356,7 @@ const routes: Routes = [ { path: "edit-send", component: SendAddEditV2Component, - canActivate: [authGuard, filePickerPopoutGuard()], + canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7988bec29b9..8f446b32197 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -3,11 +3,7 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { merge, of, Subject } from "rxjs"; -import { - CollectionService, - OrganizationUserApiService, - OrganizationUserService, -} from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -48,19 +44,13 @@ import { LogoutService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { - AutomaticUserConfirmationService, - DefaultAutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ExtensionAuthRequestAnsweringService } from "@bitwarden/browser/auth/services/auth-request-answering/extension-auth-request-answering.service"; import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service"; import { BrowserRouterService } from "@bitwarden/browser/platform/popup/services/browser-router.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { - InternalOrganizationServiceAbstraction, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService, @@ -776,19 +766,6 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionNewDeviceVerificationComponentService, deps: [], }), - safeProvider({ - provide: AutomaticUserConfirmationService, - useClass: DefaultAutomaticUserConfirmationService, - deps: [ - ConfigService, - ApiService, - OrganizationUserService, - StateProvider, - InternalOrganizationServiceAbstraction, - OrganizationUserApiService, - PolicyService, - ], - }), safeProvider({ provide: SessionTimeoutTypeService, useClass: BrowserSessionTimeoutTypeService, diff --git a/apps/browser/src/tools/popup/guards/file-picker-popout.guard.spec.ts b/apps/browser/src/tools/popup/guards/file-picker-popout.guard.spec.ts index 2f100ab67f2..64b7ac673c1 100644 --- a/apps/browser/src/tools/popup/guards/file-picker-popout.guard.spec.ts +++ b/apps/browser/src/tools/popup/guards/file-picker-popout.guard.spec.ts @@ -269,23 +269,21 @@ describe("filePickerPopoutGuard", () => { inSidebarSpy.mockReturnValue(false); }); - it.each([ - { route: "/import" }, - { route: "/add-send" }, - { route: "/edit-send" }, - { route: "/attachments" }, - ])("should open popout for $route route", async ({ route }) => { - const importState: RouterStateSnapshot = { - url: route, - } as RouterStateSnapshot; + it.each([{ route: "/import" }, { route: "/add-send" }, { route: "/attachments" }])( + "should open popout for $route route", + async ({ route }) => { + const importState: RouterStateSnapshot = { + url: route, + } as RouterStateSnapshot; - const guard = filePickerPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, importState)); + const guard = filePickerPopoutGuard(); + const result = await TestBed.runInInjectionContext(() => guard(mockRoute, importState)); - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#" + route); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - }); + expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#" + route); + expect(closePopupSpy).toHaveBeenCalledWith(window); + expect(result).toBe(false); + }, + ); }); describe("Url handling", () => { @@ -354,30 +352,6 @@ describe("filePickerPopoutGuard", () => { expect(result).toBe(true); }, ); - - it.each([ - { deviceType: DeviceType.FirefoxExtension, name: "Firefox" }, - { deviceType: DeviceType.SafariExtension, name: "Safari" }, - { deviceType: DeviceType.ChromeExtension, name: "Chrome" }, - { deviceType: DeviceType.EdgeExtension, name: "Edge" }, - ])("should allow navigation for editing text Sends on $name", async ({ deviceType }) => { - getDeviceSpy.mockReturnValue(deviceType); - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - - const editTextSendState: RouterStateSnapshot = { - url: "/edit-send?sendId=abc123&type=0", - } as RouterStateSnapshot; - - const guard = filePickerPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => - guard(mockRoute, editTextSendState), - ); - - expect(openPopoutSpy).not.toHaveBeenCalled(); - expect(closePopupSpy).not.toHaveBeenCalled(); - expect(result).toBe(true); - }); }); describe("File Sends (type=1)", () => { @@ -487,115 +461,6 @@ describe("filePickerPopoutGuard", () => { } }, ); - - it.each([ - { - deviceType: DeviceType.FirefoxExtension, - name: "Firefox", - os: "Mac", - userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", - expectPopout: true, - }, - { - deviceType: DeviceType.FirefoxExtension, - name: "Firefox", - os: "Linux", - userAgent: "Mozilla/5.0 (X11; Linux x86_64)", - expectPopout: true, - }, - { - deviceType: DeviceType.FirefoxExtension, - name: "Firefox", - os: "Windows", - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", - expectPopout: true, - }, - { - deviceType: DeviceType.SafariExtension, - name: "Safari", - os: "Mac", - userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", - expectPopout: true, - }, - { - deviceType: DeviceType.ChromeExtension, - name: "Chrome", - os: "Mac", - userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", - expectPopout: true, - }, - { - deviceType: DeviceType.ChromeExtension, - name: "Chrome", - os: "Linux", - userAgent: "Mozilla/5.0 (X11; Linux x86_64)", - expectPopout: true, - }, - { - deviceType: DeviceType.ChromeExtension, - name: "Chrome", - os: "Windows", - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", - expectPopout: false, - }, - { - deviceType: DeviceType.EdgeExtension, - name: "Edge", - os: "Mac", - userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", - expectPopout: true, - }, - { - deviceType: DeviceType.EdgeExtension, - name: "Edge", - os: "Linux", - userAgent: "Mozilla/5.0 (X11; Linux x86_64)", - expectPopout: true, - }, - { - deviceType: DeviceType.EdgeExtension, - name: "Edge", - os: "Windows", - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", - expectPopout: false, - }, - ])( - "should require popout for editing a file Send on $name $os", - async ({ deviceType, userAgent, expectPopout }) => { - getDeviceSpy.mockReturnValue(deviceType); - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - - if (userAgent) { - Object.defineProperty(window, "navigator", { - value: { userAgent, appVersion: userAgent }, - configurable: true, - writable: true, - }); - } - - const editFileSendState: RouterStateSnapshot = { - url: "/edit-send?sendId=abc123&type=1", - } as RouterStateSnapshot; - - const guard = filePickerPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => - guard(mockRoute, editFileSendState), - ); - - if (expectPopout === false) { - expect(openPopoutSpy).not.toHaveBeenCalled(); - expect(closePopupSpy).not.toHaveBeenCalled(); - expect(result).toBe(true); - } else { - expect(openPopoutSpy).toHaveBeenCalledWith( - "popup/index.html#/edit-send?sendId=abc123&type=1", - ); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - } - }, - ); }); describe("Send routes without type parameter", () => { diff --git a/apps/browser/src/tools/popup/guards/file-picker-popout.guard.ts b/apps/browser/src/tools/popup/guards/file-picker-popout.guard.ts index 900ff328ac8..48a73ed780f 100644 --- a/apps/browser/src/tools/popup/guards/file-picker-popout.guard.ts +++ b/apps/browser/src/tools/popup/guards/file-picker-popout.guard.ts @@ -24,9 +24,9 @@ import { SendType } from "@bitwarden/common/tools/send/types/send-type"; */ export function filePickerPopoutGuard(): CanActivateFn { return async (_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - // Check if this is a text Send route (no file picker needed) - if (isTextOnlySendRoute(state.url)) { - return true; // Allow navigation without popout + // Text Sends have no file picker — never require a popout regardless of browser + if (isTextSendRoute(state.url)) { + return true; } // Check if browser is one that needs popout for file pickers @@ -81,29 +81,16 @@ export function filePickerPopoutGuard(): CanActivateFn { } /** - * Determines if the route is for a text Send that doesn't require file picker display. - * - * @param url The route URL with query parameters - * @returns true if this is a Send route with explicitly text type (SendType.Text = 0) + * Returns true when the add-send route targets a Text Send (type=0). + * Text Sends have no file picker and never require a popout window. */ -function isTextOnlySendRoute(url: string): boolean { - // Only apply to Send routes - if (!url.includes("/add-send") && !url.includes("/edit-send")) { +function isTextSendRoute(url: string): boolean { + if (!url.includes("/add-send")) { return false; } - - // Parse query parameters to check Send type - const queryStartIndex = url.indexOf("?"); - if (queryStartIndex === -1) { - // No query params - default to requiring popout for safety + const queryStart = url.indexOf("?"); + if (queryStart === -1) { return false; } - - const queryString = url.substring(queryStartIndex + 1); - const params = new URLSearchParams(queryString); - const typeParam = params.get("type"); - - // Only skip popout for explicitly text-based Sends (SendType.Text = 0) - // If type is missing, null, or not text, default to requiring popout - return typeParam === String(SendType.Text); + return new URLSearchParams(url.substring(queryStart + 1)).get("type") === String(SendType.Text); } diff --git a/apps/browser/src/tools/popup/guards/firefox-popout.guard.spec.ts b/apps/browser/src/tools/popup/guards/firefox-popout.guard.spec.ts deleted file mode 100644 index df04b965d4c..00000000000 --- a/apps/browser/src/tools/popup/guards/firefox-popout.guard.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; - -import { DeviceType } from "@bitwarden/common/enums"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; -import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; - -import { firefoxPopoutGuard } from "./firefox-popout.guard"; - -describe("firefoxPopoutGuard", () => { - let getDeviceSpy: jest.SpyInstance; - let inPopoutSpy: jest.SpyInstance; - let inSidebarSpy: jest.SpyInstance; - let openPopoutSpy: jest.SpyInstance; - let closePopupSpy: jest.SpyInstance; - - const mockRoute = {} as ActivatedRouteSnapshot; - const mockState: RouterStateSnapshot = { - url: "/import?param=value", - } as RouterStateSnapshot; - - beforeEach(() => { - getDeviceSpy = jest.spyOn(BrowserPlatformUtilsService, "getDevice"); - inPopoutSpy = jest.spyOn(BrowserPopupUtils, "inPopout"); - inSidebarSpy = jest.spyOn(BrowserPopupUtils, "inSidebar"); - openPopoutSpy = jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation(); - closePopupSpy = jest.spyOn(BrowserApi, "closePopup").mockImplementation(); - - TestBed.configureTestingModule({}); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("when browser is Firefox", () => { - beforeEach(() => { - getDeviceSpy.mockReturnValue(DeviceType.FirefoxExtension); - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - }); - - it("should open popout and block navigation when not already in popout or sidebar", async () => { - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState)); - - expect(getDeviceSpy).toHaveBeenCalledWith(window); - expect(inPopoutSpy).toHaveBeenCalledWith(window); - expect(inSidebarSpy).toHaveBeenCalledWith(window); - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/import?param=value"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - }); - - it("should not add autoClosePopout parameter to the url", async () => { - const guard = firefoxPopoutGuard(); - await TestBed.runInInjectionContext(() => guard(mockRoute, mockState)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/import?param=value"); - expect(openPopoutSpy).not.toHaveBeenCalledWith(expect.stringContaining("autoClosePopout")); - }); - - it("should allow navigation when already in popout", async () => { - inPopoutSpy.mockReturnValue(true); - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState)); - - expect(openPopoutSpy).not.toHaveBeenCalled(); - expect(closePopupSpy).not.toHaveBeenCalled(); - expect(result).toBe(true); - }); - - it("should allow navigation when already in sidebar", async () => { - inSidebarSpy.mockReturnValue(true); - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState)); - - expect(openPopoutSpy).not.toHaveBeenCalled(); - expect(closePopupSpy).not.toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - - describe("when browser is not Firefox", () => { - beforeEach(() => { - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - }); - - it.each([ - { deviceType: DeviceType.ChromeExtension, name: "ChromeExtension" }, - { deviceType: DeviceType.EdgeExtension, name: "EdgeExtension" }, - { deviceType: DeviceType.OperaExtension, name: "OperaExtension" }, - { deviceType: DeviceType.SafariExtension, name: "SafariExtension" }, - { deviceType: DeviceType.VivaldiExtension, name: "VivaldiExtension" }, - ])( - "should allow navigation without opening popout when device is $name", - async ({ deviceType }) => { - getDeviceSpy.mockReturnValue(deviceType); - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState)); - - expect(getDeviceSpy).toHaveBeenCalledWith(window); - expect(openPopoutSpy).not.toHaveBeenCalled(); - expect(closePopupSpy).not.toHaveBeenCalled(); - expect(result).toBe(true); - }, - ); - }); - - describe("file picker routes", () => { - beforeEach(() => { - getDeviceSpy.mockReturnValue(DeviceType.FirefoxExtension); - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - }); - - it("should open popout for /import route", async () => { - const importState: RouterStateSnapshot = { - url: "/import", - } as RouterStateSnapshot; - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, importState)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/import"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - }); - - it("should open popout for /add-send route", async () => { - const addSendState: RouterStateSnapshot = { - url: "/add-send", - } as RouterStateSnapshot; - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, addSendState)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/add-send"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - }); - - it("should open popout for /edit-send route", async () => { - const editSendState: RouterStateSnapshot = { - url: "/edit-send", - } as RouterStateSnapshot; - - const guard = firefoxPopoutGuard(); - const result = await TestBed.runInInjectionContext(() => guard(mockRoute, editSendState)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/edit-send"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - expect(result).toBe(false); - }); - }); - - describe("url handling", () => { - beforeEach(() => { - getDeviceSpy.mockReturnValue(DeviceType.FirefoxExtension); - inPopoutSpy.mockReturnValue(false); - inSidebarSpy.mockReturnValue(false); - }); - - it("should preserve query parameters in the popout url", async () => { - const stateWithQuery: RouterStateSnapshot = { - url: "/import?foo=bar&baz=qux", - } as RouterStateSnapshot; - - const guard = firefoxPopoutGuard(); - await TestBed.runInInjectionContext(() => guard(mockRoute, stateWithQuery)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/import?foo=bar&baz=qux"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - }); - - it("should handle urls without query parameters", async () => { - const stateWithoutQuery: RouterStateSnapshot = { - url: "/simple-path", - } as RouterStateSnapshot; - - const guard = firefoxPopoutGuard(); - await TestBed.runInInjectionContext(() => guard(mockRoute, stateWithoutQuery)); - - expect(openPopoutSpy).toHaveBeenCalledWith("popup/index.html#/simple-path"); - expect(closePopupSpy).toHaveBeenCalledWith(window); - }); - }); -}); diff --git a/apps/browser/src/tools/popup/guards/firefox-popout.guard.ts b/apps/browser/src/tools/popup/guards/firefox-popout.guard.ts deleted file mode 100644 index 821f1b7a5bc..00000000000 --- a/apps/browser/src/tools/popup/guards/firefox-popout.guard.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; - -import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api"; -import BrowserPopupUtils from "@bitwarden/browser/platform/browser/browser-popup-utils"; -import { BrowserPlatformUtilsService } from "@bitwarden/browser/platform/services/platform-utils/browser-platform-utils.service"; -import { DeviceType } from "@bitwarden/common/enums"; - -/** - * Guard that forces a popout window on Firefox browser when a file picker could be exposed. - * Necessary to avoid a crash: https://bugzilla.mozilla.org/show_bug.cgi?id=1292701 - * Also disallows the user from closing a popout and re-opening the view exposing the file picker. - * - * @returns CanActivateFn that opens popout and blocks navigation on Firefox - */ -export function firefoxPopoutGuard(): CanActivateFn { - return async (_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - // Check if browser is Firefox using the platform utils service - const deviceType = BrowserPlatformUtilsService.getDevice(window); - const isFirefox = deviceType === DeviceType.FirefoxExtension; - - // Check if already in popout/sidebar - const inPopout = BrowserPopupUtils.inPopout(window); - const inSidebar = BrowserPopupUtils.inSidebar(window); - - // Open popout if on Firefox and not already in popout/sidebar - if (isFirefox && !inPopout && !inSidebar) { - // Don't add autoClosePopout for file picker scenarios - user should manually close - await BrowserPopupUtils.openPopout(`popup/index.html#${state.url}`); - - // Close the original popup window - BrowserApi.closePopup(window); - - return false; // Block navigation - popout will reload - } - - return true; // Allow navigation - }; -} diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 94c1df46eea..38ef7a4f1df 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -20,7 +20,13 @@ {{ "createdSendSuccessfully" | i18n }}

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

+ + +

+ {{ "simplifiedAutofillDescription" | i18n }} +

+ +
+} diff --git a/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.spec.ts b/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.spec.ts new file mode 100644 index 00000000000..3dfd9368109 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.spec.ts @@ -0,0 +1,131 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject, of } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { StateProvider } from "@bitwarden/state"; + +import { SimplifiedAutofillInfoComponent } from "./simplified-autofill-info.component"; + +describe("SimplifiedAutofillInfoComponent", () => { + let fixture: ComponentFixture; + + const getUserState$ = jest.fn().mockReturnValue(of(null)); + const getFeatureFlag$ = jest.fn().mockReturnValue(of(true)); + const activeAccount$ = new BehaviorSubject({ id: "test-user-id" }); + + beforeEach(async () => { + // Mock getAnimations for all span elements before any components are created + if (!HTMLSpanElement.prototype.getAnimations) { + HTMLSpanElement.prototype.getAnimations = jest.fn().mockReturnValue([]); + } + + await TestBed.configureTestingModule({ + imports: [SimplifiedAutofillInfoComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { + provide: ConfigService, + useValue: { getFeatureFlag$ }, + }, + { + provide: AccountService, + useValue: { activeAccount$: activeAccount$ }, + }, + { + provide: StateProvider, + useValue: { + getUserState$, + getUser: jest.fn().mockReturnValue({ + update: jest.fn().mockResolvedValue(undefined), + }), + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + fixture.detectChanges(); + }); + + it("sets pingElement to hidden when animation finishes", async () => { + const mockAnimation: Partial & { animationName: string } = { + animationName: "tw-ping", + onfinish: null, + }; + + // Override the mock to return our specific animation + (HTMLSpanElement.prototype.getAnimations as jest.Mock).mockReturnValue([ + mockAnimation as Animation, + ]); + + // Create a new fixture with fresh mocks that will show the ping animation + getUserState$.mockReturnValue(of({ hasSeen: false, hasDismissed: false })); + + const newFixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + + // Trigger change detection to render the template and run the effect + newFixture.detectChanges(); + await newFixture.whenStable(); + + expect(mockAnimation.onfinish).toBeDefined(); + expect(mockAnimation.onfinish).not.toBeNull(); + const onfinishHandler = mockAnimation.onfinish; + + await onfinishHandler.call(mockAnimation, null); + + const newPingElement = newFixture.nativeElement.querySelector("span"); + + expect(newPingElement.hidden).toBe(true); + }); + + describe("shouldShowIcon$", () => { + it("renders the icon button when feature flag is enabled and not dismissed", async () => { + getUserState$.mockReturnValue(of({ hasSeen: false, hasDismissed: false })); + + const newFixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + newFixture.detectChanges(); + await newFixture.whenStable(); + + const button = newFixture.nativeElement.querySelector("button[type='button']"); + expect(button).toBeTruthy(); + }); + + it("does not render icon button when dismissed", async () => { + getFeatureFlag$.mockReturnValue(of(true)); + getUserState$.mockReturnValue(of({ hasSeen: true, hasDismissed: true })); + + const newFixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + newFixture.detectChanges(); + await newFixture.whenStable(); + + const button = newFixture.nativeElement.querySelector("button[type='button']"); + expect(button).toBeFalsy(); + }); + }); + + describe("shouldShowPingAnimation$", () => { + it("renders ping animation when not seen", async () => { + getUserState$.mockReturnValue(of({ hasSeen: false, hasDismissed: false })); + + const newFixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + newFixture.detectChanges(); + await newFixture.whenStable(); + + const pingElement = newFixture.nativeElement.querySelector("span.tw-bg-primary-600"); + expect(pingElement).toBeTruthy(); + }); + + it("does not render ping animation when already seen", async () => { + getUserState$.mockReturnValue(of({ hasSeen: true, hasDismissed: false })); + + const newFixture = TestBed.createComponent(SimplifiedAutofillInfoComponent); + newFixture.detectChanges(); + await newFixture.whenStable(); + + const pingElement = newFixture.nativeElement.querySelector("span.tw-bg-primary-600"); + expect(pingElement).toBeFalsy(); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.ts b/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.ts new file mode 100644 index 00000000000..f156fb29ba2 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault/simplified-autofill-info/simplified-autofill-info.component.ts @@ -0,0 +1,110 @@ +import { AsyncPipe } from "@angular/common"; +import { + Component, + ChangeDetectionStrategy, + viewChild, + ElementRef, + inject, + effect, +} from "@angular/core"; +import { combineLatest, firstValueFrom } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { InfoFilledIcon } from "@bitwarden/assets/svg"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PopoverModule, IconModule, ButtonModule, SvgModule } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_AUTOFILL_SIMPLIFIED_ICON } from "@bitwarden/state"; + +const VAULT_AUTOFILL_SIMPLIFIED_ICON_KEY = new UserKeyDefinition<{ + hasSeen: boolean; + hasDismissed: boolean; +}>(VAULT_AUTOFILL_SIMPLIFIED_ICON, "vaultAutofillSimplifiedIcon", { + deserializer: (value) => value, + clearOn: [], +}); + +@Component({ + selector: "app-simplified-autofill-info", + templateUrl: "./simplified-autofill-info.component.html", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [JslibModule, PopoverModule, IconModule, ButtonModule, SvgModule, AsyncPipe], +}) +export class SimplifiedAutofillInfoComponent { + private configService = inject(ConfigService); + private stateProvider = inject(StateProvider); + private accountService = inject(AccountService); + + readonly pingElement = viewChild>("pingElement"); + protected readonly InfoFilledIcon = InfoFilledIcon; + + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + + private vaultAutofillSimplifiedIconState$ = this.userId$.pipe( + switchMap((userId) => + this.stateProvider.getUserState$(VAULT_AUTOFILL_SIMPLIFIED_ICON_KEY, userId), + ), + ); + + protected shouldShowPingAnimation$ = this.vaultAutofillSimplifiedIconState$.pipe( + map((state) => !state?.hasSeen), + ); + + /** Emits true when the icon should be shown to the user */ + protected shouldShowIcon$ = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.PM31039ItemActionInExtension), + this.vaultAutofillSimplifiedIconState$, + ]).pipe( + map(([isFeatureEnabled, state]) => { + if (!isFeatureEnabled) { + return false; + } + + return !state?.hasDismissed; + }), + ); + + constructor() { + // Set up animation handler when ping element becomes available + effect(() => { + const pingElement = this.pingElement()?.nativeElement; + if (!pingElement) { + return; + } + + const animation = pingElement + .getAnimations() + .find((a) => "animationName" in a && a.animationName === "tw-ping"); + if (animation) { + animation.onfinish = () => { + // Set the ping element to hidden after the animation finishes to avoid any alignment issues with the icon. + pingElement.hidden = true; + void this.updateUserState({ hasSeen: true, hasDismissed: false }); + }; + } + }); + } + + /** Update the user state when the popover closes */ + protected async onPopoverClose(): Promise { + await this.updateUserState({ hasDismissed: true, hasSeen: true }); + } + + /** Updates the user's state for the simplified autofill icon */ + private async updateUserState(newState: { + hasSeen: boolean; + hasDismissed: boolean; + }): Promise { + const userId = await firstValueFrom(this.userId$); + + const state = this.stateProvider.getUser(userId, VAULT_AUTOFILL_SIMPLIFIED_ICON_KEY); + await state.update((oldState) => ({ + ...oldState, + ...newState, + })); + } +} diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html index e9e89776dde..ff6935df2a6 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.html @@ -44,6 +44,7 @@ (click)="onRefresh.emit()" [label]="'refresh' | i18n" > + - + } @if (showAutofillBadge()) { diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts index eda84265e90..22eb12e3d05 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.spec.ts @@ -84,11 +84,12 @@ describe("VaultListItemsContainerComponent", () => { { provide: CipherService, useValue: mock() }, { provide: Router, useValue: { navigate: jest.fn() } }, { provide: PlatformUtilsService, useValue: { getAutofillKeyboardShortcut: () => "" } }, - { provide: DialogService, useValue: mock() }, { provide: PasswordRepromptService, useValue: mock() }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideProvider(DialogService, { useValue: mock() }) + .compileComponents(); fixture = TestBed.createComponent(VaultListItemsContainerComponent); component = fixture.componentInstance; diff --git a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts index fb8d20c5cf6..331ea799169 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-list-items-container/vault-list-items-container.component.ts @@ -302,8 +302,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit { if (this.currentUriIsBlocked()) { return false; } - return this.isAutofillList() - ? this.simplifiedItemActionEnabled() + + return this.simplifiedItemActionEnabled() + ? this.isAutofillList() : this.primaryActionAutofill(); }); diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.html b/apps/browser/src/vault/popup/components/vault/vault.component.html index 28abb92b8a9..2f43d29d776 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault.component.html @@ -127,7 +127,7 @@ @if (cipher) { - + + @if (showAutofillButton()) { + + } + } diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts index 5c94af0205d..6710e7dc238 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from "@angular/core import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { of, Subject } from "rxjs"; +import { BehaviorSubject, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -18,6 +18,7 @@ import { import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,6 +34,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -45,8 +47,12 @@ import { import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { VaultPopupAfterDeletionNavigationService } from "../../../services/vault-popup-after-deletion-navigation.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; -import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; import { ViewComponent } from "./view.component"; @@ -62,13 +68,16 @@ describe("ViewComponent", () => { const mockNavigate = jest.fn(); const collect = jest.fn().mockResolvedValue(null); const doAutofill = jest.fn().mockResolvedValue(true); + const doAutofillAndSave = jest.fn().mockResolvedValue(true); const copy = jest.fn().mockResolvedValue(true); const back = jest.fn().mockResolvedValue(null); const openSimpleDialog = jest.fn().mockResolvedValue(true); - const stop = jest.fn(); + const navigateAfterDeletion = jest.fn().mockResolvedValue(undefined); const showToast = jest.fn(); const showPasswordPrompt = jest.fn().mockResolvedValue(true); const getFeatureFlag$ = jest.fn().mockReturnValue(of(true)); + const getFeatureFlag = jest.fn().mockResolvedValue(true); + const currentAutofillTab$ = of({ url: "https://example.com", id: 1 }); const mockCipher = { id: "122-333-444", @@ -87,8 +96,12 @@ describe("ViewComponent", () => { const mockPasswordRepromptService = { showPasswordPrompt, }; + const autofillAllowed$ = new BehaviorSubject(true); const mockVaultPopupAutofillService = { doAutofill, + doAutofillAndSave, + currentAutofillTab$, + autofillAllowed$, }; const mockCopyCipherFieldService = { copy, @@ -112,12 +125,15 @@ describe("ViewComponent", () => { mockNavigate.mockClear(); collect.mockClear(); doAutofill.mockClear(); + doAutofillAndSave.mockClear(); copy.mockClear(); - stop.mockClear(); + navigateAfterDeletion.mockClear(); openSimpleDialog.mockClear(); back.mockClear(); showToast.mockClear(); showPasswordPrompt.mockClear(); + getFeatureFlag.mockClear(); + autofillAllowed$.next(true); cipherArchiveService.hasArchiveFlagEnabled$ = of(true); cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); cipherArchiveService.archiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); @@ -134,10 +150,13 @@ describe("ViewComponent", () => { { provide: PopupRouterCacheService, useValue: mock({ back }) }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: EventCollectionService, useValue: { collect } }, - { provide: VaultPopupScrollPositionService, useValue: { stop } }, + { + provide: VaultPopupAfterDeletionNavigationService, + useValue: { navigateAfterDeletion }, + }, { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, { provide: ToastService, useValue: { showToast } }, - { provide: ConfigService, useValue: { getFeatureFlag$ } }, + { provide: ConfigService, useValue: { getFeatureFlag$, getFeatureFlag } }, { provide: I18nService, useValue: { @@ -203,6 +222,8 @@ describe("ViewComponent", () => { provide: DomainSettingsService, useValue: { showFavicons$: of(true), + resolvedDefaultUriMatchStrategy$: of(UriMatchStrategy.Domain), + getUrlEquivalentDomains: jest.fn().mockReturnValue(of([])), }, }, { @@ -543,17 +564,10 @@ describe("ViewComponent", () => { expect(openSimpleDialog).toHaveBeenCalledTimes(1); }); - it("navigates back", async () => { + it("navigates after deletion", async () => { await component.delete(); - expect(back).toHaveBeenCalledTimes(1); - }); - - it("stops scroll position service", async () => { - await component.delete(); - - expect(stop).toHaveBeenCalledTimes(1); - expect(stop).toHaveBeenCalledWith(true); + expect(navigateAfterDeletion).toHaveBeenCalledTimes(1); }); describe("deny confirmation", () => { @@ -569,8 +583,7 @@ describe("ViewComponent", () => { }); it("does not interact with side effects", () => { - expect(back).not.toHaveBeenCalled(); - expect(stop).not.toHaveBeenCalled(); + expect(navigateAfterDeletion).not.toHaveBeenCalled(); expect(showToast).not.toHaveBeenCalled(); }); }); @@ -697,4 +710,452 @@ describe("ViewComponent", () => { expect(badge).toBeFalsy(); }); }); + + describe("showAutofillButton", () => { + beforeEach(() => { + component.cipher = { ...mockCipher, type: CipherType.Login } as CipherView; + }); + + it("returns true when feature flag is enabled, cipher is a login, and not archived/deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Card type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Card, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns true for Identity type when conditions are met", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Identity, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(true); + })); + + it("returns false when feature flag is disabled", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(false)); + + // Recreate component to pick up the new feature flag value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when autofill is not allowed", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(false); + + // Recreate component to pick up the new autofillAllowed value + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SecureNote type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SecureNote, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false for SshKey type", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.SshKey, + isArchived: false, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is archived", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: true, + isDeleted: false, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + + it("returns false when cipher is deleted", fakeAsync(() => { + getFeatureFlag$.mockReturnValue(of(true)); + autofillAllowed$.next(true); + + // Recreate component to pick up the signal values + fixture = TestBed.createComponent(ViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + isArchived: false, + isDeleted: true, + } as CipherView; + + flush(); + + const result = component.showAutofillButton(); + + expect(result).toBe(false); + })); + }); + + describe("doAutofill", () => { + let dialogService: DialogService; + const originalCurrentAutofillTab$ = currentAutofillTab$; + + beforeEach(() => { + dialogService = TestBed.inject(DialogService); + + component.cipher = { + ...mockCipher, + type: CipherType.Login, + login: { + username: "test", + password: "test", + uris: [ + { + uri: "https://example.com", + match: null, + } as LoginUriView, + ], + }, + edit: true, + } as CipherView; + }); + + afterEach(() => { + // Restore original observable to prevent test pollution + mockVaultPopupAutofillService.currentAutofillTab$ = originalCurrentAutofillTab$; + }); + + it("returns early when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValue(false); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when no URIs and default strategy is Exact", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = []; + (component as any).uriMatchStrategy$ = of(UriMatchStrategy.Exact); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows exact match dialog when all URIs have exact match strategy", async () => { + getFeatureFlag.mockResolvedValue(true); + component.cipher.login.uris = [ + { uri: "https://example.com", match: UriMatchStrategy.Exact } as LoginUriView, + { uri: "https://example2.com", match: UriMatchStrategy.Exact } as LoginUriView, + ]; + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("shows error dialog when current tab URL is unavailable", async () => { + getFeatureFlag.mockResolvedValue(true); + mockVaultPopupAutofillService.currentAutofillTab$ = of({ url: null, id: 1 }); + + await component.doAutofill(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("autofills directly when domain matches", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(true); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + }); + + it("shows confirmation dialog when domain does not match", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(AutofillConfirmationDialogComponent.open).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: false, + }, + }); + }); + + it("does not autofill when user cancels confirmation dialog", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).not.toHaveBeenCalled(); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills only when user selects AutofilledOnly", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofilledOnly), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofill).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("autofills and saves URL when user selects AutofillAndUrlAdded", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.AutofillAndUrlAdded), + }; + jest.spyOn(AutofillConfirmationDialogComponent, "open").mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(doAutofillAndSave).toHaveBeenCalledWith(component.cipher, true, true); + expect(doAutofill).not.toHaveBeenCalled(); + }); + + it("passes viewOnly as true when cipher is not editable", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.edit = false; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com"], + viewOnly: true, + }, + }); + }); + + it("filters out URIs without uri property", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = [ + { uri: "https://example.com" } as LoginUriView, + { uri: null } as LoginUriView, + { uri: "https://example2.com" } as LoginUriView, + ]; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: ["https://example.com", "https://example2.com"], + viewOnly: false, + }, + }); + }); + + it("handles cipher with no login uris gracefully", async () => { + getFeatureFlag.mockResolvedValue(true); + jest.spyOn(component as any, "_domainMatched").mockResolvedValue(false); + component.cipher.login.uris = null; + + const mockDialogRef = { + closed: of(AutofillConfirmationDialogResult.Canceled), + }; + const openSpy = jest + .spyOn(AutofillConfirmationDialogComponent, "open") + .mockReturnValue(mockDialogRef as any); + + await component.doAutofill(); + + expect(openSpy).toHaveBeenCalledWith(dialogService, { + data: { + currentUrl: "https://example.com", + savedUrls: [], + viewOnly: false, + }, + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault/view/view.component.ts b/apps/browser/src/vault/popup/components/vault/view/view.component.ts index 48402a957d6..6dda738c4a4 100644 --- a/apps/browser/src/vault/popup/components/vault/view/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view/view.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Observable, switchMap, of, map } from "rxjs"; @@ -21,7 +21,11 @@ import { SHOW_AUTOFILL_BUTTON, UPDATE_PASSWORD, } from "@bitwarden/common/autofill/constants"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -32,6 +36,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { AsyncActionsModule, @@ -62,10 +67,16 @@ import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-p import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service"; +import { + ROUTES_AFTER_EDIT_DELETION, + VaultPopupAfterDeletionNavigationService, +} from "../../../services/vault-popup-after-deletion-navigation.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; -import { VaultPopupScrollPositionService } from "../../../services/vault-popup-scroll-position.service"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../../utils/vault-popout-window"; -import { ROUTES_AFTER_EDIT_DELETION } from "../add-edit/add-edit.component"; +import { + AutofillConfirmationDialogComponent, + AutofillConfirmationDialogResult, +} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component"; /** * The types of actions that can be triggered when loading the view vault item popout via the @@ -118,6 +129,13 @@ export class ViewComponent { senderTabId?: number; routeAfterDeletion?: ROUTES_AFTER_EDIT_DELETION; + //feature flag + private readonly pm30521FeatureFlag = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM30521_AutofillButtonViewLoginScreen), + ); + + private readonly autofillAllowed = toSignal(this.vaultPopupAutofillService.autofillAllowed$); + private uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$; protected showFooter$: Observable; protected userCanArchive$ = this.accountService.activeAccount$ .pipe(getUserId) @@ -139,9 +157,11 @@ export class ViewComponent { private popupRouterCacheService: PopupRouterCacheService, protected cipherAuthorizationService: CipherAuthorizationService, private copyCipherFieldService: CopyCipherFieldService, - private popupScrollPositionService: VaultPopupScrollPositionService, private archiveService: CipherArchiveService, private archiveCipherUtilsService: ArchiveCipherUtilitiesService, + private domainSettingsService: DomainSettingsService, + private configService: ConfigService, + private afterDeletionNavigationService: VaultPopupAfterDeletionNavigationService, ) { this.subscribeToParams(); } @@ -264,8 +284,7 @@ export class ViewComponent { return false; } - this.popupScrollPositionService.stop(true); - await this.popupRouterCacheService.back(); + await this.afterDeletionNavigationService.navigateAfterDeletion(this.routeAfterDeletion); this.toastService.showToast({ variant: "success", @@ -322,6 +341,113 @@ export class ViewComponent { : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId); } + showAutofillButton(): boolean { + //feature flag + if (!this.pm30521FeatureFlag()) { + return false; + } + + if (!this.autofillAllowed()) { + return false; + } + + const validAutofillType = ( + [CipherType.Login, CipherType.Card, CipherType.Identity] as CipherType[] + ).includes(CipherViewLikeUtils.getType(this.cipher)); + + return validAutofillType && !(this.cipher.isArchived || this.cipher.isDeleted); + } + + async doAutofill() { + //feature flag + if ( + !(await this.configService.getFeatureFlag(FeatureFlag.PM30521_AutofillButtonViewLoginScreen)) + ) { + return; + } + + //for non login types that are still auto-fillable + if (CipherViewLikeUtils.getType(this.cipher) !== CipherType.Login) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const uris = this.cipher.login?.uris ?? []; + const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); + + const showExactMatchDialog = + uris.length === 0 + ? uriMatchStrategy === UriMatchStrategy.Exact + : // all saved URIs are exact match + uris.every((u) => (u.match ?? uriMatchStrategy) === UriMatchStrategy.Exact); + + if (showExactMatchDialog) { + await this.dialogService.openSimpleDialog({ + title: { key: "cannotAutofill" }, + content: { key: "cannotAutofillExactMatch" }, + type: "info", + acceptButtonText: { key: "okay" }, + cancelButtonText: null, + }); + return; + } + + //this tab checking should be moved into the vault-popup-autofill service in case the current tab is changed + //ticket: https://bitwarden.atlassian.net/browse/PM-32467 + const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$); + + if (!currentTab?.url) { + await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: { key: "errorGettingAutoFillData" }, + type: "danger", + }); + return; + } + + if (await this._domainMatched(currentTab)) { + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + } + + const ref = AutofillConfirmationDialogComponent.open(this.dialogService, { + data: { + currentUrl: currentTab?.url || "", + savedUrls: this.cipher.login?.uris?.filter((u) => u.uri).map((u) => u.uri!) ?? [], + viewOnly: !this.cipher.edit, + }, + }); + + const result = await firstValueFrom(ref.closed); + + switch (result) { + case AutofillConfirmationDialogResult.Canceled: + return; + case AutofillConfirmationDialogResult.AutofilledOnly: + await this.vaultPopupAutofillService.doAutofill(this.cipher, true, true); + return; + case AutofillConfirmationDialogResult.AutofillAndUrlAdded: + await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, true, true); + return; + } + } + + private async _domainMatched(currentTab: chrome.tabs.Tab): Promise { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(currentTab?.url), + ); + const defaultMatch = await firstValueFrom( + this.domainSettingsService.resolvedDefaultUriMatchStrategy$, + ); + + return CipherViewLikeUtils.matchesUri( + this.cipher, + currentTab?.url, + equivalentDomains, + defaultMatch, + ); + } + /** * Handles the load action for the view vault item popout. These actions are typically triggered * via the extension context menu. It is necessary to render the view for items that have password diff --git a/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.spec.ts new file mode 100644 index 00000000000..56309eeadf2 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.spec.ts @@ -0,0 +1,123 @@ +import { Location } from "@angular/common"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; +import { RouteHistoryCacheState } from "../../../platform/services/popup-view-cache-background.service"; + +import { + ROUTES_AFTER_EDIT_DELETION, + VaultPopupAfterDeletionNavigationService, +} from "./vault-popup-after-deletion-navigation.service"; +import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service"; + +describe("VaultPopupAfterDeletionNavigationService", () => { + let service: VaultPopupAfterDeletionNavigationService; + + let router: MockProxy; + let location: MockProxy; + let popupRouterCacheService: MockProxy; + let scrollPositionService: MockProxy; + let platformUtilsService: MockProxy; + + beforeEach(() => { + router = mock(); + location = mock(); + popupRouterCacheService = mock(); + scrollPositionService = mock(); + platformUtilsService = mock(); + + router.navigate.mockResolvedValue(true); + platformUtilsService.isFirefox.mockReturnValue(false); + + TestBed.configureTestingModule({ + providers: [ + VaultPopupAfterDeletionNavigationService, + { provide: Router, useValue: router }, + { provide: Location, useValue: location }, + { provide: PopupRouterCacheService, useValue: popupRouterCacheService }, + { provide: VaultPopupScrollPositionService, useValue: scrollPositionService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + ], + }); + + service = TestBed.inject(VaultPopupAfterDeletionNavigationService); + }); + + describe("navigateAfterDeletion", () => { + describe("scroll position reset", () => { + it("stops the scroll position service on non-Firefox browsers", async () => { + platformUtilsService.isFirefox.mockReturnValue(false); + + await service.navigateAfterDeletion(); + + expect(scrollPositionService.stop).toHaveBeenCalledWith(true); + }); + + it("does not stop the scroll position service on Firefox", async () => { + platformUtilsService.isFirefox.mockReturnValue(true); + + await service.navigateAfterDeletion(); + + expect(scrollPositionService.stop).not.toHaveBeenCalled(); + }); + }); + + describe("default route (tabsVault)", () => { + it("navigates to the vault tab by default", async () => { + await service.navigateAfterDeletion(); + + expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + + it("navigates to the vault tab when explicitly provided", async () => { + await service.navigateAfterDeletion(ROUTES_AFTER_EDIT_DELETION.tabsVault); + + expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + + it("does not check popup history", async () => { + await service.navigateAfterDeletion(ROUTES_AFTER_EDIT_DELETION.tabsVault); + + expect(popupRouterCacheService.history$).not.toHaveBeenCalled(); + }); + }); + + describe("non-default route", () => { + const historyWithArchive: RouteHistoryCacheState[] = [ + { url: "/tabs/vault" } as RouteHistoryCacheState, + { url: "/archive" } as RouteHistoryCacheState, + { url: "/view-cipher" } as RouteHistoryCacheState, + { url: "/edit-cipher" } as RouteHistoryCacheState, + ]; + + it("walks back through history when the route is found", async () => { + popupRouterCacheService.history$.mockReturnValue(of(historyWithArchive)); + + await service.navigateAfterDeletion(ROUTES_AFTER_EDIT_DELETION.archive); + + // archive is at index 1, current is index 3 (length - 1), so stepsBack = 1 - 3 = -2 + expect(location.historyGo).toHaveBeenCalledWith(-2); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it("falls back to router.navigate when the route is not in history", async () => { + const historyWithoutArchive: RouteHistoryCacheState[] = [ + { url: "/tabs/vault" } as RouteHistoryCacheState, + { url: "/view-cipher" } as RouteHistoryCacheState, + { url: "/edit-cipher" } as RouteHistoryCacheState, + ]; + popupRouterCacheService.history$.mockReturnValue(of(historyWithoutArchive)); + + await service.navigateAfterDeletion(ROUTES_AFTER_EDIT_DELETION.archive); + + expect(location.historyGo).not.toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(["/archive"]); + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.ts b/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.ts new file mode 100644 index 00000000000..0577072fb37 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-after-deletion-navigation.service.ts @@ -0,0 +1,76 @@ +import { Location } from "@angular/common"; +import { inject, Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; + +import { VaultPopupScrollPositionService } from "./vault-popup-scroll-position.service"; + +/** + * Available routes to navigate to after deleting a cipher. + * Useful when the user could be coming from a different view other than the main vault (e.g., archive). + */ +export const ROUTES_AFTER_EDIT_DELETION = Object.freeze({ + tabsVault: "/tabs/vault", + archive: "/archive", +} as const); + +export type ROUTES_AFTER_EDIT_DELETION = + (typeof ROUTES_AFTER_EDIT_DELETION)[keyof typeof ROUTES_AFTER_EDIT_DELETION]; + +/** + * Service that handles navigation after a cipher is deleted. + * + * When the deletion target route is somewhere other than the default vault tab, + * this service walks back through the popup history to find it (preserving the + * browser-extension back-button behaviour). If the route is not found in + * history it falls back to a normal `Router.navigate`. + */ +@Injectable({ + providedIn: "root", +}) +export class VaultPopupAfterDeletionNavigationService { + private router = inject(Router); + private location = inject(Location); + private popupRouterCacheService = inject(PopupRouterCacheService); + private scrollPositionService = inject(VaultPopupScrollPositionService); + private platformUtilsService = inject(PlatformUtilsService); + + /** + * Navigate to the appropriate route after a cipher has been deleted. + * Resets the vault scroll position on non-Firefox browsers to prevent + * auto-scrolling to a stale position. Firefox is excluded because eagerly + * clearing scroll state triggers its native scroll restoration, causing + * unwanted scroll behavior. + * + * @param routeAfterDeletion - The target route to navigate to. Defaults to the main vault tab. + */ + async navigateAfterDeletion( + routeAfterDeletion: ROUTES_AFTER_EDIT_DELETION = ROUTES_AFTER_EDIT_DELETION.tabsVault, + ): Promise { + if (!this.platformUtilsService.isFirefox()) { + this.scrollPositionService.stop(true); + } + + if (routeAfterDeletion !== ROUTES_AFTER_EDIT_DELETION.tabsVault) { + const history = await firstValueFrom(this.popupRouterCacheService.history$()); + const targetIndex = history.map((h) => h.url).lastIndexOf(routeAfterDeletion); + + if (targetIndex !== -1) { + const stepsBack = targetIndex - (history.length - 1); + // Use historyGo to navigate back to the target route in history. + // This allows downstream calls to `back()` to continue working as expected. + this.location.historyGo(stepsBack); + return; + } + + await this.router.navigate([routeAfterDeletion]); + return; + } + + await this.router.navigate([routeAfterDeletion]); + } +} diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 845dfd6f4b1..79b0cc63a4b 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -370,37 +370,6 @@ describe("VaultPopupItemsService", () => { }); }); - describe("remainingCiphers$", () => { - beforeEach(() => { - searchService.isSearchable.mockImplementation(async (text) => text.length > 2); - }); - - it("should exclude autofill and favorite ciphers", (done) => { - service.remainingCiphers$.subscribe((ciphers) => { - // 2 autofill ciphers, 2 favorite ciphers = 6 remaining ciphers to show - expect(ciphers.length).toBe(6); - done(); - }); - }); - - it("should filter remainingCiphers$ down to search term", (done) => { - const cipherList = Object.values(allCiphers); - const searchText = "Login"; - - searchService.searchCiphers.mockImplementation(async () => { - return cipherList.filter((cipher) => { - return cipher.name.includes(searchText); - }); - }); - - service.remainingCiphers$.subscribe((ciphers) => { - // There are 6 remaining ciphers but only 2 with "Login" in the name - expect(ciphers.length).toBe(2); - done(); - }); - }); - }); - describe("emptyVault$", () => { it("should return true if there are no ciphers", (done) => { cipherServiceMock.cipherListViews$.mockReturnValue(of([])); @@ -493,8 +462,8 @@ describe("VaultPopupItemsService", () => { // Start tracking loading$ emissions tracked = new ObservableTracker(service.loading$); - // Track remainingCiphers$ to make cipher observables active - trackedCiphers = new ObservableTracker(service.remainingCiphers$); + // Track favoriteCiphers$ to make cipher observables active + trackedCiphers = new ObservableTracker(service.favoriteCiphers$); }); it("should initialize with true first", async () => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 93f2734e6b8..0055d683f22 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -2,7 +2,6 @@ import { inject, Injectable, NgZone } from "@angular/core"; import { toObservable } from "@angular/core/rxjs-interop"; import { combineLatest, - concatMap, distinctUntilChanged, distinctUntilKeyChanged, filter, @@ -119,7 +118,7 @@ export class VaultPopupItemsService { this.cipherService .cipherListViews$(userId) .pipe(filter((ciphers) => ciphers != null)), - this.cipherService.failedToDecryptCiphers$(userId), + this.cipherService.failedToDecryptCiphers$(userId).pipe(startWith([])), this.restrictedItemTypesService.restricted$, ]), ), @@ -242,31 +241,12 @@ export class VaultPopupItemsService { shareReplay({ refCount: false, bufferSize: 1 }), ); - /** - * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite. - * Ciphers are sorted by name. - */ - remainingCiphers$: Observable = this.favoriteCiphers$.pipe( - concatMap( - ( - favoriteCiphers, // concatMap->of is used to make withLatestFrom lazy to avoid race conditions with autoFillCiphers$ - ) => - of(favoriteCiphers).pipe(withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$)), - ), - map(([favoriteCiphers, ciphers, autoFillCiphers]) => - ciphers.filter( - (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), - ), - ), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - /** * Observable that indicates whether the service is currently loading ciphers. */ loading$: Observable = merge( this._ciphersLoading$.pipe(map(() => true)), - this.remainingCiphers$.pipe(map(() => false)), + this.favoriteCiphers$.pipe(map(() => false)), ).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); /** Observable that indicates whether there is search text present. diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts index f7b4e7b473a..2a9ebdcddf6 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts @@ -7,6 +7,8 @@ import { of } from "rxjs"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { AutoConfirmState, AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -52,6 +54,8 @@ describe("AdminSettingsComponent", () => { let autoConfirmService: MockProxy; let nudgesService: MockProxy; let mockDialogService: MockProxy; + let eventCollectionService: MockProxy; + let organizationService: MockProxy; const userId = "test-user-id" as UserId; const mockAutoConfirmState: AutoConfirmState = { @@ -64,10 +68,14 @@ describe("AdminSettingsComponent", () => { autoConfirmService = mock(); nudgesService = mock(); mockDialogService = mock(); + eventCollectionService = mock(); + organizationService = mock(); autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState)); autoConfirmService.upsert.mockResolvedValue(undefined); nudgesService.showNudgeSpotlight$.mockReturnValue(of(false)); + eventCollectionService.collect.mockResolvedValue(undefined); + organizationService.organizations$.mockReturnValue(of([])); await TestBed.configureTestingModule({ imports: [AdminSettingsComponent], @@ -77,6 +85,11 @@ describe("AdminSettingsComponent", () => { { provide: AutomaticUserConfirmationService, useValue: autoConfirmService }, { provide: DialogService, useValue: mockDialogService }, { provide: NudgesService, useValue: nudgesService }, + { provide: EventCollectionService, useValue: eventCollectionService }, + { + provide: InternalOrganizationServiceAbstraction, + useValue: organizationService, + }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], }) diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts index e4b676525ed..99cb5a814c1 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -16,12 +16,15 @@ import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotligh import { AutoConfirmWarningDialogComponent, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { EventType } from "@bitwarden/common/enums"; import { BitIconButtonComponent, CardComponent, @@ -69,6 +72,8 @@ export class AdminSettingsComponent implements OnInit { private destroyRef: DestroyRef, private dialogService: DialogService, private nudgesService: NudgesService, + private eventCollectionService: EventCollectionService, + private organizationService: InternalOrganizationServiceAbstraction, ) {} async ngOnInit() { @@ -88,14 +93,26 @@ export class AdminSettingsComponent implements OnInit { } return of(false); }), - withLatestFrom(this.autoConfirmService.configuration$(userId)), - switchMap(([newValue, existingState]) => - this.autoConfirmService.upsert(userId, { + withLatestFrom( + this.autoConfirmService.configuration$(userId), + this.organizationService.organizations$(userId), + ), + switchMap(async ([newValue, existingState, organizations]) => { + await this.autoConfirmService.upsert(userId, { ...existingState, enabled: newValue, showBrowserNotification: false, - }), - ), + }); + + // Auto-confirm users can only belong to one organization + const organization = organizations[0]; + if (organization?.id) { + const eventType = newValue + ? EventType.Organization_AutoConfirmEnabled_Admin + : EventType.Organization_AutoConfirmDisabled_Admin; + await this.eventCollectionService.collect(eventType, undefined, true, organization.id); + } + }), takeUntilDestroyed(this.destroyRef), ) .subscribe(); diff --git a/apps/browser/src/vault/popup/settings/archive.component.spec.ts b/apps/browser/src/vault/popup/settings/archive.component.spec.ts index bbb61b57a84..810ab85c4c6 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.spec.ts @@ -62,7 +62,6 @@ describe("ArchiveComponent", () => { useValue: { hasOrganizations, organizations$: () => of([]) }, }, { provide: CollectionService, useValue: { decryptedCollections$ } }, - { provide: DialogService, useValue: mock() }, { provide: CipherService, useValue: mock() }, { provide: CipherArchiveService, @@ -99,7 +98,9 @@ describe("ArchiveComponent", () => { }, }, ], - }).compileComponents(); + }) + .overrideProvider(DialogService, { useValue: mock() }) + .compileComponents(); fixture = TestBed.createComponent(ArchiveComponent); component = fixture.componentInstance; diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index 0d1baa56a21..38fa3855969 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -42,7 +42,7 @@ import { import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { ROUTES_AFTER_EDIT_DELETION } from "../components/vault/add-edit/add-edit.component"; +import { ROUTES_AFTER_EDIT_DELETION } from "../services/vault-popup-after-deletion-navigation.service"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection diff --git a/apps/browser/src/vault/popup/settings/folders.component.spec.ts b/apps/browser/src/vault/popup/settings/folders.component.spec.ts index 678e6d3f10e..7e08cc684a1 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.spec.ts @@ -94,11 +94,12 @@ describe("FoldersComponent", () => { fixture.detectChanges(); }); - it("removes the last option in the folder array", (done) => { + it("should show all folders", (done) => { component.folders$.subscribe((folders) => { expect(folders).toEqual([ { id: "1", name: "Folder 1" }, { id: "2", name: "Folder 2" }, + { id: "0", name: "No Folder" }, ]); done(); }); diff --git a/apps/browser/src/vault/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts index b70c17bd6a5..a38f6630949 100644 --- a/apps/browser/src/vault/popup/settings/folders.component.ts +++ b/apps/browser/src/vault/popup/settings/folders.component.ts @@ -53,13 +53,6 @@ export class FoldersComponent { this.folders$ = this.activeUserId$.pipe( filter((userId): userId is UserId => userId !== null), switchMap((userId) => this.folderService.folderViews$(userId)), - map((folders) => { - // Remove the last folder, which is the "no folder" option folder - if (folders.length > 0) { - return folders.slice(0, folders.length - 1); - } - return folders; - }), ); } diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index b2a95ed5689..07864dcc1f1 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden – Upravitelj gesel - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Doma, na delu ali na poti – Bitwarden na enostaven način zaščiti vsa vaša gesla, ključe za dostop in občutljive podatke. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav diff --git a/apps/cli/package.json b/apps/cli/package.json index 6c27267054f..a5b3a00ec4e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2026.1.0", + "version": "2026.2.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index db070344628..cc01c21e0d0 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { filter, firstValueFrom, map, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 18079bd2409..824b03b99cf 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -35,6 +35,9 @@ "invalidVerificationCode": { "message": "Invalid verification code." }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "masterPassRequired": { "message": "Master password is required." }, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index b5a2b1b8196..6bbb7614c78 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -664,7 +664,7 @@ export class ServiceContainer { this.accountService, this.kdfConfigService, this.keyService, - this.securityStateService, + this.accountCryptographicStateService, this.apiService, this.stateProvider, this.configService, @@ -737,8 +737,6 @@ export class ServiceContainer { this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.stateProvider, - this.platformUtilsService, - this.apiService, ); this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); @@ -863,6 +861,7 @@ export class ServiceContainer { this.pinService, this.kdfConfigService, new CliBiometricsService(), + this.masterPasswordUnlockService, ); const biometricService = new CliBiometricsService(); diff --git a/apps/cli/src/utils.ts b/apps/cli/src/utils.ts index 72746cb9b71..3aafcea6346 100644 --- a/apps/cli/src/utils.ts +++ b/apps/cli/src/utils.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import * as fs from "fs"; import * as path from "path"; diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index e1a91966afd..32f3b3aa547 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -170,7 +170,7 @@ export class CreateCommand { const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, - new Uint8Array(fileBuf).buffer, + new Uint8Array(fileBuf), activeUserId, ); const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId); diff --git a/apps/cli/src/vault/delete.command.ts b/apps/cli/src/vault/delete.command.ts index 8df1f8f316e..c92379e058f 100644 --- a/apps/cli/src/vault/delete.command.ts +++ b/apps/cli/src/vault/delete.command.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; diff --git a/apps/cli/src/vault/models/attachment.response.ts b/apps/cli/src/vault/models/attachment.response.ts index c4450fa8def..c7e88df1c39 100644 --- a/apps/cli/src/vault/models/attachment.response.ts +++ b/apps/cli/src/vault/models/attachment.response.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; export class AttachmentResponse { diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml index 166b852588b..8a5c36e7da6 100644 --- a/apps/desktop/custom-appx-manifest.xml +++ b/apps/desktop/custom-appx-manifest.xml @@ -1,17 +1,9 @@ - + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> + @@ -87,8 +80,9 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re - + + @@ -106,6 +100,13 @@ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/re + + + + Bitwarden + + + diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 3f4f10a1fcf..f120408a9e5 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -21,7 +21,32 @@ impl super::BiometricTrait for Biometric { async fn prompt(_hwnd: Vec, _message: String) -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let result = proxy .check_authorization( diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs index ef6527e7b26..2656bd3fdf9 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/linux.rs @@ -96,7 +96,32 @@ async fn polkit_authenticate_bitwarden_policy() -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + + // Use system-bus-name instead of unix-process to avoid PID namespace issues in + // sandboxed environments (e.g., Flatpak). When using unix-process with a PID from + // inside the sandbox, polkit cannot validate it against the host PID namespace. + // + // By using system-bus-name, polkit queries D-Bus for the connection's credentials, + // which includes the correct host PID and UID, avoiding namespace mismatches. + // + // If D-Bus unique name is not available, fall back to the traditional unix-process + // approach for compatibility with non-sandboxed environments. + let subject = if let Some(bus_name) = connection.unique_name() { + use zbus::zvariant::{OwnedValue, Str}; + let mut subject_details = std::collections::HashMap::new(); + subject_details.insert( + "name".to_string(), + OwnedValue::from(Str::from(bus_name.as_str())), + ); + Subject { + subject_kind: "system-bus-name".to_string(), + subject_details, + } + } else { + // Fallback: use unix-process with PID (may not work in sandboxed environments) + Subject::new_for_owner(std::process::id(), None, None)? + }; + let details = std::collections::HashMap::new(); let authorization_result = proxy .check_authorization( diff --git a/apps/desktop/desktop_native/napi/src/autofill.rs b/apps/desktop/desktop_native/napi/src/autofill.rs deleted file mode 100644 index 7717b22ccef..00000000000 --- a/apps/desktop/desktop_native/napi/src/autofill.rs +++ /dev/null @@ -1,332 +0,0 @@ -#[napi] -pub mod autofill { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use serde::{de::DeserializeOwned, Deserialize, Serialize}; - use tracing::error; - - #[napi] - pub async fn run_command(value: String) -> napi::Result { - desktop_core::autofill::run_command(value) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[derive(Debug, serde::Serialize, serde:: Deserialize)] - pub enum BitwardenError { - Internal(String), - } - - #[napi(string_enum)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub enum UserVerification { - #[napi(value = "preferred")] - Preferred, - #[napi(value = "required")] - Required, - #[napi(value = "discouraged")] - Discouraged, - } - - #[derive(Serialize, Deserialize)] - #[serde(bound = "T: Serialize + DeserializeOwned")] - pub struct PasskeyMessage { - pub sequence_number: u32, - pub value: Result, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Position { - pub x: i32, - pub y: i32, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationRequest { - pub rp_id: String, - pub user_name: String, - pub user_handle: Vec, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub supported_algorithms: Vec, - pub window_xy: Position, - pub excluded_credentials: Vec>, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyRegistrationResponse { - pub rp_id: String, - pub client_data_hash: Vec, - pub credential_id: Vec, - pub attestation_object: Vec, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionRequest { - pub rp_id: String, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub allowed_credentials: Vec>, - pub window_xy: Position, - //extension_input: Vec, TODO: Implement support for extensions - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionWithoutUserInterfaceRequest { - pub rp_id: String, - pub credential_id: Vec, - pub user_name: String, - pub user_handle: Vec, - pub record_identifier: Option, - pub client_data_hash: Vec, - pub user_verification: UserVerification, - pub window_xy: Position, - } - - #[napi(object)] - #[derive(Debug, Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct NativeStatus { - pub key: String, - pub value: String, - } - - #[napi(object)] - #[derive(Serialize, Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct PasskeyAssertionResponse { - pub rp_id: String, - pub user_handle: Vec, - pub signature: Vec, - pub client_data_hash: Vec, - pub authenticator_data: Vec, - pub credential_id: Vec, - } - - #[napi] - pub struct AutofillIpcServer { - server: desktop_core::ipc::server::Server, - } - - // FIXME: Remove unwraps! They panic and terminate the whole application. - #[allow(clippy::unwrap_used)] - #[napi] - impl AutofillIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - // Ideally we'd have a single callback that has an enum containing the request values, - // but NAPI doesn't support that just yet - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" - )] - registration_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyRegistrationRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" - )] - assertion_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" - )] - assertion_without_user_interface_callback: ThreadsafeFunction< - FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, - >, - #[napi( - ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" - )] - native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(Message { - client_id, - kind, - message, - }) = recv.recv().await - { - match kind { - // TODO: We're ignoring the connection and disconnection messages for now - MessageType::Connected | MessageType::Disconnected => continue, - MessageType::Message => { - let Some(message) = message else { - error!("Message is empty"); - continue; - }; - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::< - PasskeyMessage, - >(&message) - { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - - assertion_without_user_interface_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message1"); - } - } - - match serde_json::from_str::>( - &message, - ) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value).into()) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - registration_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(e) => { - error!(error = %e, "Error deserializing message2"); - } - } - - match serde_json::from_str::>(&message) { - Ok(msg) => { - let value = msg - .value - .map(|value| (client_id, msg.sequence_number, value)) - .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); - native_status_callback - .call(value, ThreadsafeFunctionCallMode::NonBlocking); - continue; - } - Err(error) => { - error!(%error, "Unable to deserialze native status."); - } - } - - error!(message, "Received an unknown message2"); - } - } - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(AutofillIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - #[napi] - pub fn complete_registration( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyRegistrationResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_assertion( - &self, - client_id: u32, - sequence_number: u32, - response: PasskeyAssertionResponse, - ) -> napi::Result { - let message = PasskeyMessage { - sequence_number, - value: Ok(response), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - #[napi] - pub fn complete_error( - &self, - client_id: u32, - sequence_number: u32, - error: String, - ) -> napi::Result { - let message: PasskeyMessage<()> = PasskeyMessage { - sequence_number, - value: Err(BitwardenError::Internal(error)), - }; - self.send(client_id, serde_json::to_string(&message).unwrap()) - } - - // TODO: Add a way to send a message to a specific client? - fn send(&self, _client_id: u32, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/autostart.rs b/apps/desktop/desktop_native/napi/src/autostart.rs deleted file mode 100644 index 3068226809e..00000000000 --- a/apps/desktop/desktop_native/napi/src/autostart.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod autostart { - #[napi] - pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { - desktop_core::autostart::set_autostart(autostart, params) - .await - .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/autotype.rs b/apps/desktop/desktop_native/napi/src/autotype.rs deleted file mode 100644 index b63c95ceb5c..00000000000 --- a/apps/desktop/desktop_native/napi/src/autotype.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[napi] -pub mod autotype { - #[napi] - pub fn get_foreground_window_title() -> napi::Result { - autotype::get_foreground_window_title().map_err(|_| { - napi::Error::from_reason( - "Autotype Error: failed to get foreground window title".to_string(), - ) - }) - } - - #[napi] - pub fn type_input( - input: Vec, - keyboard_shortcut: Vec, - ) -> napi::Result<(), napi::Status> { - autotype::type_input(&input, &keyboard_shortcut) - .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics.rs b/apps/desktop/desktop_native/napi/src/biometrics.rs deleted file mode 100644 index bca802d5884..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[napi] -pub mod biometrics { - use desktop_core::biometric::{Biometric, BiometricTrait}; - - // Prompt for biometric confirmation - #[napi] - pub async fn prompt( - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - Biometric::prompt(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn available() -> napi::Result { - Biometric::available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn set_biometric_secret( - service: String, - account: String, - secret: String, - key_material: Option, - iv_b64: String, - ) -> napi::Result { - Biometric::set_biometric_secret( - &service, - &account, - &secret, - key_material.map(|m| m.into()), - &iv_b64, - ) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Retrieves the biometric secret for the given service and account. - /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. - #[napi] - pub async fn get_biometric_secret( - service: String, - account: String, - key_material: Option, - ) -> napi::Result { - Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Derives key material from biometric data. Returns a string encoded with a - /// base64 encoded key and the base64 encoded challenge used to create it - /// separated by a `|` character. - /// - /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will - /// be generated. - /// - /// `format!("|")` - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn derive_key_material(iv: Option) -> napi::Result { - Biometric::derive_key_material(iv.as_deref()) - .map(|k| k.into()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi(object)] - pub struct KeyMaterial { - pub os_key_part_b64: String, - pub client_key_part_b64: Option, - } - - impl From for desktop_core::biometric::KeyMaterial { - fn from(km: KeyMaterial) -> Self { - desktop_core::biometric::KeyMaterial { - os_key_part_b64: km.os_key_part_b64, - client_key_part_b64: km.client_key_part_b64, - } - } - } - - #[napi(object)] - pub struct OsDerivedKey { - pub key_b64: String, - pub iv_b64: String, - } - - impl From for OsDerivedKey { - fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { - OsDerivedKey { - key_b64: km.key_b64, - iv_b64: km.iv_b64, - } - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs b/apps/desktop/desktop_native/napi/src/biometrics_v2.rs deleted file mode 100644 index 2df3a6a07be..00000000000 --- a/apps/desktop/desktop_native/napi/src/biometrics_v2.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod biometrics_v2 { - use desktop_core::biometric_v2::BiometricTrait; - - #[napi] - pub struct BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem, - } - - #[napi] - pub fn init_biometric_system() -> napi::Result { - Ok(BiometricLockSystem { - inner: desktop_core::biometric_v2::BiometricLockSystem::new(), - }) - } - - #[napi] - pub async fn authenticate( - biometric_lock_system: &BiometricLockSystem, - hwnd: napi::bindgen_prelude::Buffer, - message: String, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate(hwnd.into(), message) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn authenticate_available( - biometric_lock_system: &BiometricLockSystem, - ) -> napi::Result { - biometric_lock_system - .inner - .authenticate_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn enroll_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .enroll_persistent(&user_id, &key) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn provide_key( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - key: napi::bindgen_prelude::Buffer, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .provide_key(&user_id, &key) - .await; - Ok(()) - } - - #[napi] - pub async fn unlock( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - hwnd: napi::bindgen_prelude::Buffer, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock(&user_id, hwnd.into()) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - .map(|v| v.into()) - } - - #[napi] - pub async fn unlock_available( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .unlock_available(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn has_persistent( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result { - biometric_lock_system - .inner - .has_persistent(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn unenroll( - biometric_lock_system: &BiometricLockSystem, - user_id: String, - ) -> napi::Result<()> { - biometric_lock_system - .inner - .unenroll(&user_id) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/chromium_importer.rs b/apps/desktop/desktop_native/napi/src/chromium_importer.rs deleted file mode 100644 index da295984a47..00000000000 --- a/apps/desktop/desktop_native/napi/src/chromium_importer.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[napi] -pub mod chromium_importer { - use std::collections::HashMap; - - use chromium_importer::{ - chromium::{ - DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, - ProfileInfo as _ProfileInfo, - }, - metadata::NativeImporterMetadata as _NativeImporterMetadata, - }; - - #[napi(object)] - pub struct ProfileInfo { - pub id: String, - pub name: String, - } - - #[napi(object)] - pub struct Login { - pub url: String, - pub username: String, - pub password: String, - pub note: String, - } - - #[napi(object)] - pub struct LoginImportFailure { - pub url: String, - pub username: String, - pub error: String, - } - - #[napi(object)] - pub struct LoginImportResult { - pub login: Option, - pub failure: Option, - } - - #[napi(object)] - pub struct NativeImporterMetadata { - pub id: String, - pub loaders: Vec, - pub instructions: String, - } - - impl From<_LoginImportResult> for LoginImportResult { - fn from(l: _LoginImportResult) -> Self { - match l { - _LoginImportResult::Success(l) => LoginImportResult { - login: Some(Login { - url: l.url, - username: l.username, - password: l.password, - note: l.note, - }), - failure: None, - }, - _LoginImportResult::Failure(l) => LoginImportResult { - login: None, - failure: Some(LoginImportFailure { - url: l.url, - username: l.username, - error: l.error, - }), - }, - } - } - } - - impl From<_ProfileInfo> for ProfileInfo { - fn from(p: _ProfileInfo) -> Self { - ProfileInfo { - id: p.folder, - name: p.name, - } - } - } - - impl From<_NativeImporterMetadata> for NativeImporterMetadata { - fn from(m: _NativeImporterMetadata) -> Self { - NativeImporterMetadata { - id: m.id, - loaders: m.loaders, - instructions: m.instructions, - } - } - } - - #[napi] - /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. - pub fn get_metadata() -> HashMap { - chromium_importer::metadata::get_supported_importers::() - .into_iter() - .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) - .collect() - } - - #[napi] - pub fn get_available_profiles(browser: String) -> napi::Result> { - chromium_importer::chromium::get_available_profiles(&browser) - .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub async fn import_logins( - browser: String, - profile_id: String, - ) -> napi::Result> { - chromium_importer::chromium::import_logins(&browser, &profile_id) - .await - .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/clipboards.rs b/apps/desktop/desktop_native/napi/src/clipboards.rs deleted file mode 100644 index 810e457dd60..00000000000 --- a/apps/desktop/desktop_native/napi/src/clipboards.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[napi] -pub mod clipboards { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn read() -> napi::Result { - desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn write(text: String, password: bool) -> napi::Result<()> { - desktop_core::clipboard::write(&text, password) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/ipc.rs b/apps/desktop/desktop_native/napi/src/ipc.rs deleted file mode 100644 index ba72b1dce2b..00000000000 --- a/apps/desktop/desktop_native/napi/src/ipc.rs +++ /dev/null @@ -1,106 +0,0 @@ -#[napi] -pub mod ipc { - use desktop_core::ipc::server::{Message, MessageType}; - use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; - - #[napi(object)] - pub struct IpcMessage { - pub client_id: u32, - pub kind: IpcMessageType, - pub message: Option, - } - - impl From for IpcMessage { - fn from(message: Message) -> Self { - IpcMessage { - client_id: message.client_id, - kind: message.kind.into(), - message: message.message, - } - } - } - - #[napi] - pub enum IpcMessageType { - Connected, - Disconnected, - Message, - } - - impl From for IpcMessageType { - fn from(message_type: MessageType) -> Self { - match message_type { - MessageType::Connected => IpcMessageType::Connected, - MessageType::Disconnected => IpcMessageType::Disconnected, - MessageType::Message => IpcMessageType::Message, - } - } - } - - #[napi] - pub struct NativeIpcServer { - server: desktop_core::ipc::server::Server, - } - - #[napi] - impl NativeIpcServer { - /// Create and start the IPC server without blocking. - /// - /// @param name The endpoint name to listen on. This name uniquely identifies the IPC - /// connection and must be the same for both the server and client. @param callback - /// This function will be called whenever a message is received from a client. - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi(factory)] - pub async fn listen( - name: String, - #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] - callback: ThreadsafeFunction, - ) -> napi::Result { - let (send, mut recv) = tokio::sync::mpsc::channel::(32); - tokio::spawn(async move { - while let Some(message) = recv.recv().await { - callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - - let path = desktop_core::ipc::path(&name); - - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { - napi::Error::from_reason(format!( - "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" - )) - })?; - - Ok(NativeIpcServer { server }) - } - - /// Return the path to the IPC server. - #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() - } - - /// Stop the IPC server. - #[napi] - pub fn stop(&self) -> napi::Result<()> { - self.server.stop(); - Ok(()) - } - - /// Send a message over the IPC server to all the connected clients - /// - /// @return The number of clients that the message was sent to. Note that the number of - /// messages actually received may be less, as some clients could disconnect before - /// receiving the message. - #[napi] - pub fn send(&self, message: String) -> napi::Result { - self.server - .send(message) - .map_err(|e| { - napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) - }) - // NAPI doesn't support u64 or usize, so we need to convert to u32 - .map(|u| u32::try_from(u).unwrap_or_default()) - } - } -} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index e3abfd50e7a..588f757631c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -4,22 +4,1244 @@ extern crate napi_derive; mod passkey_authenticator_internal; mod registry; -// NAPI namespaces -// In each of these modules, the types are defined within a nested namespace of -// the same name so that NAPI can export the TypeScript types within a -// namespace. -pub mod autofill; -pub mod autostart; -pub mod autotype; -pub mod biometrics; -pub mod biometrics_v2; -pub mod chromium_importer; -pub mod clipboards; -pub mod ipc; -pub mod logging; -pub mod passkey_authenticator; -pub mod passwords; -pub mod powermonitors; -pub mod processisolations; -pub mod sshagent; -pub mod windows_registry; +#[napi] +pub mod passwords { + /// The error message returned when a password is not found during retrieval or deletion. + #[napi] + pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; + + /// Fetch the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn get_password(service: String, account: String) -> napi::Result { + desktop_core::password::get_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Save the password to the keychain. Adds an entry if none exists otherwise updates the + /// existing entry. + #[napi] + pub async fn set_password( + service: String, + account: String, + password: String, + ) -> napi::Result<()> { + desktop_core::password::set_password(&service, &account, &password) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Delete the stored password from the keychain. + /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. + #[napi] + pub async fn delete_password(service: String, account: String) -> napi::Result<()> { + desktop_core::password::delete_password(&service, &account) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Checks if the os secure storage is available + #[napi] + pub async fn is_available() -> napi::Result { + desktop_core::password::is_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod biometrics { + use desktop_core::biometric::{Biometric, BiometricTrait}; + + // Prompt for biometric confirmation + #[napi] + pub async fn prompt( + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + Biometric::prompt(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn available() -> napi::Result { + Biometric::available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn set_biometric_secret( + service: String, + account: String, + secret: String, + key_material: Option, + iv_b64: String, + ) -> napi::Result { + Biometric::set_biometric_secret( + &service, + &account, + &secret, + key_material.map(|m| m.into()), + &iv_b64, + ) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Retrieves the biometric secret for the given service and account. + /// Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist. + #[napi] + pub async fn get_biometric_secret( + service: String, + account: String, + key_material: Option, + ) -> napi::Result { + Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Derives key material from biometric data. Returns a string encoded with a + /// base64 encoded key and the base64 encoded challenge used to create it + /// separated by a `|` character. + /// + /// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will + /// be generated. + /// + /// `format!("|")` + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn derive_key_material(iv: Option) -> napi::Result { + Biometric::derive_key_material(iv.as_deref()) + .map(|k| k.into()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi(object)] + pub struct KeyMaterial { + pub os_key_part_b64: String, + pub client_key_part_b64: Option, + } + + impl From for desktop_core::biometric::KeyMaterial { + fn from(km: KeyMaterial) -> Self { + desktop_core::biometric::KeyMaterial { + os_key_part_b64: km.os_key_part_b64, + client_key_part_b64: km.client_key_part_b64, + } + } + } + + #[napi(object)] + pub struct OsDerivedKey { + pub key_b64: String, + pub iv_b64: String, + } + + impl From for OsDerivedKey { + fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { + OsDerivedKey { + key_b64: km.key_b64, + iv_b64: km.iv_b64, + } + } + } +} + +#[napi] +pub mod biometrics_v2 { + use desktop_core::biometric_v2::BiometricTrait; + + #[napi] + pub struct BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem, + } + + #[napi] + pub fn init_biometric_system() -> napi::Result { + Ok(BiometricLockSystem { + inner: desktop_core::biometric_v2::BiometricLockSystem::new(), + }) + } + + #[napi] + pub async fn authenticate( + biometric_lock_system: &BiometricLockSystem, + hwnd: napi::bindgen_prelude::Buffer, + message: String, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate(hwnd.into(), message) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn authenticate_available( + biometric_lock_system: &BiometricLockSystem, + ) -> napi::Result { + biometric_lock_system + .inner + .authenticate_available() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn enroll_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .enroll_persistent(&user_id, &key) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn provide_key( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + key: napi::bindgen_prelude::Buffer, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .provide_key(&user_id, &key) + .await; + Ok(()) + } + + #[napi] + pub async fn unlock( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + hwnd: napi::bindgen_prelude::Buffer, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock(&user_id, hwnd.into()) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + .map(|v| v.into()) + } + + #[napi] + pub async fn unlock_available( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .unlock_available(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn has_persistent( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result { + biometric_lock_system + .inner + .has_persistent(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn unenroll( + biometric_lock_system: &BiometricLockSystem, + user_id: String, + ) -> napi::Result<()> { + biometric_lock_system + .inner + .unenroll(&user_id) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod clipboards { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn read() -> napi::Result { + desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn write(text: String, password: bool) -> napi::Result<()> { + desktop_core::clipboard::write(&text, password) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod sshagent { + use std::sync::Arc; + + use napi::{ + bindgen_prelude::Promise, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tokio::{self, sync::Mutex}; + use tracing::error; + + #[napi] + pub struct SshAgentState { + state: desktop_core::ssh_agent::BitwardenDesktopAgent, + } + + #[napi(object)] + pub struct PrivateKey { + pub private_key: String, + pub name: String, + pub cipher_id: String, + } + + #[napi(object)] + pub struct SshKey { + pub private_key: String, + pub public_key: String, + pub key_fingerprint: String, + } + + #[napi(object)] + pub struct SshUIRequest { + pub cipher_id: Option, + pub is_list: bool, + pub process_name: String, + pub is_forwarding: bool, + pub namespace: Option, + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn serve( + callback: ThreadsafeFunction>, + ) -> napi::Result { + let (auth_request_tx, mut auth_request_rx) = + tokio::sync::mpsc::channel::(32); + let (auth_response_tx, auth_response_rx) = + tokio::sync::broadcast::channel::<(u32, bool)>(32); + let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); + // Wrap callback in Arc so it can be shared across spawned tasks + let callback = Arc::new(callback); + tokio::spawn(async move { + let _ = auth_response_rx; + + while let Some(request) = auth_request_rx.recv().await { + let cloned_response_tx_arc = auth_response_tx_arc.clone(); + let cloned_callback = callback.clone(); + tokio::spawn(async move { + let auth_response_tx_arc = cloned_response_tx_arc; + let callback = cloned_callback; + // In NAPI v3, obtain the JS callback return as a Promise and await it + // in Rust + let (tx, rx) = std::sync::mpsc::channel::>(); + let status = callback.call_with_return_value( + Ok(SshUIRequest { + cipher_id: request.cipher_id, + is_list: request.is_list, + process_name: request.process_name, + is_forwarding: request.is_forwarding, + namespace: request.namespace, + }), + ThreadsafeFunctionCallMode::Blocking, + move |ret: Result, napi::Error>, _env| { + if let Ok(p) = ret { + let _ = tx.send(p); + } + Ok(()) + }, + ); + + let result = if status == napi::Status::Ok { + match rx.recv() { + Ok(promise) => match promise.await { + Ok(v) => v, + Err(e) => { + error!(error = %e, "UI callback promise rejected"); + false + } + }, + Err(e) => { + error!(error = %e, "Failed to receive UI callback promise"); + false + } + } + } else { + error!(error = ?status, "Calling UI callback failed"); + false + }; + + let _ = auth_response_tx_arc + .lock() + .await + .send((request.request_id, result)) + .expect("should be able to send auth response to agent"); + }); + } + }); + + match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( + auth_request_tx, + Arc::new(Mutex::new(auth_response_rx)), + ) { + Ok(state) => Ok(SshAgentState { state }), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + #[napi] + pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state.stop(); + Ok(()) + } + + #[napi] + pub fn is_running(agent_state: &mut SshAgentState) -> bool { + let bitwarden_agent_state = agent_state.state.clone(); + bitwarden_agent_state.is_running() + } + + #[napi] + pub fn set_keys( + agent_state: &mut SshAgentState, + new_keys: Vec, + ) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .set_keys( + new_keys + .iter() + .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) + .collect(), + ) + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + Ok(()) + } + + #[napi] + pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .lock() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { + let bitwarden_agent_state = &mut agent_state.state; + bitwarden_agent_state + .clear_keys() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod processisolations { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn disable_coredumps() -> napi::Result<()> { + desktop_core::process_isolation::disable_coredumps() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn is_core_dumping_disabled() -> napi::Result { + desktop_core::process_isolation::is_core_dumping_disabled() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn isolate_process() -> napi::Result<()> { + desktop_core::process_isolation::isolate_process() + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod powermonitors { + use napi::{ + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + tokio, + }; + + #[napi] + pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { + let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); + desktop_core::powermonitor::on_lock(tx) + .await + .map_err(|e| napi::Error::from_reason(e.to_string()))?; + tokio::spawn(async move { + while let Some(()) = rx.recv().await { + callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + Ok(()) + } + + #[napi] + pub async fn is_lock_monitor_available() -> napi::Result { + Ok(desktop_core::powermonitor::is_lock_monitor_available().await) + } +} + +#[napi] +pub mod windows_registry { + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { + crate::registry::create_key(&key, &subkey, &value) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi] + pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { + crate::registry::delete_key(&key, &subkey) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod ipc { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; + + #[napi(object)] + pub struct IpcMessage { + pub client_id: u32, + pub kind: IpcMessageType, + pub message: Option, + } + + impl From for IpcMessage { + fn from(message: Message) -> Self { + IpcMessage { + client_id: message.client_id, + kind: message.kind.into(), + message: message.message, + } + } + } + + #[napi] + pub enum IpcMessageType { + Connected, + Disconnected, + Message, + } + + impl From for IpcMessageType { + fn from(message_type: MessageType) -> Self { + match message_type { + MessageType::Connected => IpcMessageType::Connected, + MessageType::Disconnected => IpcMessageType::Disconnected, + MessageType::Message => IpcMessageType::Message, + } + } + } + + #[napi] + pub struct NativeIpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl NativeIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + #[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")] + callback: ThreadsafeFunction, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(message) = recv.recv().await { + callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking); + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(NativeIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + /// Send a message over the IPC server to all the connected clients + /// + /// @return The number of clients that the message was sent to. Note that the number of + /// messages actually received may be less, as some clients could disconnect before + /// receiving the message. + #[napi] + pub fn send(&self, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod autostart { + #[napi] + pub async fn set_autostart(autostart: bool, params: Vec) -> napi::Result<()> { + desktop_core::autostart::set_autostart(autostart, params) + .await + .map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}"))) + } +} + +#[napi] +pub mod autofill { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use tracing::error; + + #[napi] + pub async fn run_command(value: String) -> napi::Result { + desktop_core::autofill::run_command(value) + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[derive(Debug, serde::Serialize, serde:: Deserialize)] + pub enum BitwardenError { + Internal(String), + } + + #[napi(string_enum)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum UserVerification { + #[napi(value = "preferred")] + Preferred, + #[napi(value = "required")] + Required, + #[napi(value = "discouraged")] + Discouraged, + } + + #[derive(Serialize, Deserialize)] + #[serde(bound = "T: Serialize + DeserializeOwned")] + pub struct PasskeyMessage { + pub sequence_number: u32, + pub value: Result, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Position { + pub x: i32, + pub y: i32, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + pub window_xy: Position, + pub excluded_credentials: Vec>, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub allowed_credentials: Vec>, + pub window_xy: Position, + //extension_input: Vec, TODO: Implement support for extensions + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionWithoutUserInterfaceRequest { + pub rp_id: String, + pub credential_id: Vec, + pub user_name: String, + pub user_handle: Vec, + pub record_identifier: Option, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub window_xy: Position, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct NativeStatus { + pub key: String, + pub value: String, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, + } + + #[napi] + pub struct AutofillIpcServer { + server: desktop_core::ipc::server::Server, + } + + // FIXME: Remove unwraps! They panic and terminate the whole application. + #[allow(clippy::unwrap_used)] + #[napi] + impl AutofillIpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC + /// connection and must be the same for both the server and client. @param callback + /// This function will be called whenever a message is received from a client. + #[allow(clippy::unused_async)] // FIXME: Remove unused async! + #[napi(factory)] + pub async fn listen( + name: String, + // Ideally we'd have a single callback that has an enum containing the request values, + // but NAPI doesn't support that just yet + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" + )] + registration_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyRegistrationRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" + )] + assertion_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void" + )] + assertion_without_user_interface_callback: ThreadsafeFunction< + FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void" + )] + native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(Message { + client_id, + kind, + message, + }) = recv.recv().await + { + match kind { + // TODO: We're ignoring the connection and disconnection messages for now + MessageType::Connected | MessageType::Disconnected => continue, + MessageType::Message => { + let Some(message) = message else { + error!("Message is empty"); + continue; + }; + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::< + PasskeyMessage, + >(&message) + { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_without_user_interface_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message1"); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value).into()) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + registration_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + error!(error = %e, "Error deserializing message2"); + } + } + + match serde_json::from_str::>(&message) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + native_status_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(error) => { + error!(%error, "Unable to deserialze native status."); + } + } + + error!(message, "Received an unknown message2"); + } + } + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(AutofillIpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + #[napi] + pub fn complete_registration( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyRegistrationResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_assertion( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyAssertionResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_error( + &self, + client_id: u32, + sequence_number: u32, + error: String, + ) -> napi::Result { + let message: PasskeyMessage<()> = PasskeyMessage { + sequence_number, + value: Err(BitwardenError::Internal(error)), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + // TODO: Add a way to send a message to a specific client? + fn send(&self, _client_id: u32, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } +} + +#[napi] +pub mod passkey_authenticator { + #[napi] + pub fn register() -> napi::Result<()> { + crate::passkey_authenticator_internal::register().map_err(|e| { + napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) + }) + } +} + +#[napi] +pub mod logging { + //! `logging` is the interface between the native desktop's usage of the `tracing` crate + //! for logging, to intercept events and write to the JS space. + //! + //! # Example + //! + //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting + //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} + + use std::{fmt::Write, sync::OnceLock}; + + use napi::{ + bindgen_prelude::FnArgs, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, + }; + use tracing::Level; + use tracing_subscriber::{ + filter::EnvFilter, + fmt::format::{DefaultVisitor, Writer}, + layer::SubscriberExt, + util::SubscriberInitExt, + Layer, + }; + + struct JsLogger(OnceLock>>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From<&Level> for LogLevel { + fn from(level: &Level) -> Self { + match *level { + Level::TRACE => LogLevel::Trace, + Level::DEBUG => LogLevel::Debug, + Level::INFO => LogLevel::Info, + Level::WARN => LogLevel::Warn, + Level::ERROR => LogLevel::Error, + } + } + } + + // JsLayer lets us intercept events and write them to the JS Logger. + struct JsLayer; + + impl Layer for JsLayer + where + S: tracing::Subscriber, + { + // This function builds a log message buffer from the event data and + // calls the JS logger with it. + // + // For example, this log call: + // + // ``` + // mod supreme { + // mod module { + // let foo = "bar"; + // info!(best_variable_name = %foo, "Foo done it again."); + // } + // } + // ``` + // + // , results in the following string: + // + // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let mut buffer = String::new(); + + // create the preamble text that precedes the message and vars. e.g.: + // [INFO] desktop_core::ssh_agent::platform_ssh_agent: + let level = event.metadata().level().as_str(); + let module_path = event.metadata().module_path().unwrap_or_default(); + + write!(&mut buffer, "[{level}] {module_path}:") + .expect("Failed to write tracing event to buffer"); + + let writer = Writer::new(&mut buffer); + + // DefaultVisitor adds the message and variables to the buffer + let mut visitor = DefaultVisitor::new(writer, false); + event.record(&mut visitor); + + let msg = (event.metadata().level().into(), buffer); + + if let Some(logger) = JS_LOGGER.0.get() { + let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); + }; + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { + let _ = JS_LOGGER.0.set(js_log_fn); + + // the log level hierarchy is determined by: + // - if RUST_LOG is detected at runtime + // - if RUST_LOG is provided at compile time + // - default to INFO + let filter = EnvFilter::builder() + .with_default_directive( + option_env!("RUST_LOG") + .unwrap_or("info") + .parse() + .expect("should provide valid log level at compile time."), + ) + // parse directives from the RUST_LOG environment variable, + // overriding the default directive for matching targets. + .from_env_lossy(); + + // With the `tracing-log` feature enabled for the `tracing_subscriber`, + // the registry below will initialize a log compatibility layer, which allows + // the subscriber to consume log::Records as though they were tracing Events. + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init + tracing_subscriber::registry() + .with(filter) + .with(JsLayer) + .init(); + } +} + +#[napi] +pub mod chromium_importer { + use std::collections::HashMap; + + use chromium_importer::{ + chromium::{ + DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, + ProfileInfo as _ProfileInfo, + }, + metadata::NativeImporterMetadata as _NativeImporterMetadata, + }; + + #[napi(object)] + pub struct ProfileInfo { + pub id: String, + pub name: String, + } + + #[napi(object)] + pub struct Login { + pub url: String, + pub username: String, + pub password: String, + pub note: String, + } + + #[napi(object)] + pub struct LoginImportFailure { + pub url: String, + pub username: String, + pub error: String, + } + + #[napi(object)] + pub struct LoginImportResult { + pub login: Option, + pub failure: Option, + } + + #[napi(object)] + pub struct NativeImporterMetadata { + pub id: String, + pub loaders: Vec, + pub instructions: String, + } + + impl From<_LoginImportResult> for LoginImportResult { + fn from(l: _LoginImportResult) -> Self { + match l { + _LoginImportResult::Success(l) => LoginImportResult { + login: Some(Login { + url: l.url, + username: l.username, + password: l.password, + note: l.note, + }), + failure: None, + }, + _LoginImportResult::Failure(l) => LoginImportResult { + login: None, + failure: Some(LoginImportFailure { + url: l.url, + username: l.username, + error: l.error, + }), + }, + } + } + } + + impl From<_ProfileInfo> for ProfileInfo { + fn from(p: _ProfileInfo) -> Self { + ProfileInfo { + id: p.folder, + name: p.name, + } + } + } + + impl From<_NativeImporterMetadata> for NativeImporterMetadata { + fn from(m: _NativeImporterMetadata) -> Self { + NativeImporterMetadata { + id: m.id, + loaders: m.loaders, + instructions: m.instructions, + } + } + } + + #[napi] + /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. + pub fn get_metadata() -> HashMap { + chromium_importer::metadata::get_supported_importers::() + .into_iter() + .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) + .collect() + } + + #[napi] + pub fn get_available_profiles(browser: String) -> napi::Result> { + chromium_importer::chromium::get_available_profiles(&browser) + .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + #[napi] + pub async fn import_logins( + browser: String, + profile_id: String, + ) -> napi::Result> { + chromium_importer::chromium::import_logins(&browser, &profile_id) + .await + .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} + +#[napi] +pub mod autotype { + #[napi] + pub fn get_foreground_window_title() -> napi::Result { + autotype::get_foreground_window_title().map_err(|_| { + napi::Error::from_reason( + "Autotype Error: failed to get foreground window title".to_string(), + ) + }) + } + + #[napi] + pub fn type_input( + input: Vec, + keyboard_shortcut: Vec, + ) -> napi::Result<(), napi::Status> { + autotype::type_input(&input, &keyboard_shortcut) + .map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}"))) + } +} diff --git a/apps/desktop/desktop_native/napi/src/logging.rs b/apps/desktop/desktop_native/napi/src/logging.rs deleted file mode 100644 index e5791065e4e..00000000000 --- a/apps/desktop/desktop_native/napi/src/logging.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[napi] -pub mod logging { - //! `logging` is the interface between the native desktop's usage of the `tracing` crate - //! for logging, to intercept events and write to the JS space. - //! - //! # Example - //! - //! [Elec] 14:34:03.517 › [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting - //! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock} - - use std::{fmt::Write, sync::OnceLock}; - - use napi::{ - bindgen_prelude::FnArgs, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tracing::Level; - use tracing_subscriber::{ - filter::EnvFilter, - fmt::format::{DefaultVisitor, Writer}, - layer::SubscriberExt, - util::SubscriberInitExt, - Layer, - }; - - struct JsLogger(OnceLock>>); - static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); - - #[napi] - pub enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, - } - - impl From<&Level> for LogLevel { - fn from(level: &Level) -> Self { - match *level { - Level::TRACE => LogLevel::Trace, - Level::DEBUG => LogLevel::Debug, - Level::INFO => LogLevel::Info, - Level::WARN => LogLevel::Warn, - Level::ERROR => LogLevel::Error, - } - } - } - - // JsLayer lets us intercept events and write them to the JS Logger. - struct JsLayer; - - impl Layer for JsLayer - where - S: tracing::Subscriber, - { - // This function builds a log message buffer from the event data and - // calls the JS logger with it. - // - // For example, this log call: - // - // ``` - // mod supreme { - // mod module { - // let foo = "bar"; - // info!(best_variable_name = %foo, "Foo done it again."); - // } - // } - // ``` - // - // , results in the following string: - // - // [INFO] supreme::module: Foo done it again. {best_variable_name=bar} - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - let mut buffer = String::new(); - - // create the preamble text that precedes the message and vars. e.g.: - // [INFO] desktop_core::ssh_agent::platform_ssh_agent: - let level = event.metadata().level().as_str(); - let module_path = event.metadata().module_path().unwrap_or_default(); - - write!(&mut buffer, "[{level}] {module_path}:") - .expect("Failed to write tracing event to buffer"); - - let writer = Writer::new(&mut buffer); - - // DefaultVisitor adds the message and variables to the buffer - let mut visitor = DefaultVisitor::new(writer, false); - event.record(&mut visitor); - - let msg = (event.metadata().level().into(), buffer); - - if let Some(logger) = JS_LOGGER.0.get() { - let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking); - }; - } - } - - #[napi] - pub fn init_napi_log(js_log_fn: ThreadsafeFunction>) { - let _ = JS_LOGGER.0.set(js_log_fn); - - // the log level hierarchy is determined by: - // - if RUST_LOG is detected at runtime - // - if RUST_LOG is provided at compile time - // - default to INFO - let filter = EnvFilter::builder() - .with_default_directive( - option_env!("RUST_LOG") - .unwrap_or("info") - .parse() - .expect("should provide valid log level at compile time."), - ) - // parse directives from the RUST_LOG environment variable, - // overriding the default directive for matching targets. - .from_env_lossy(); - - // With the `tracing-log` feature enabled for the `tracing_subscriber`, - // the registry below will initialize a log compatibility layer, which allows - // the subscriber to consume log::Records as though they were tracing Events. - // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init - tracing_subscriber::registry() - .with(filter) - .with(JsLayer) - .init(); - } -} diff --git a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs b/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs deleted file mode 100644 index 37796353b80..00000000000 --- a/apps/desktop/desktop_native/napi/src/passkey_authenticator.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[napi] -pub mod passkey_authenticator { - #[napi] - pub fn register() -> napi::Result<()> { - crate::passkey_authenticator_internal::register().map_err(|e| { - napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}")) - }) - } -} diff --git a/apps/desktop/desktop_native/napi/src/passwords.rs b/apps/desktop/desktop_native/napi/src/passwords.rs deleted file mode 100644 index 763f338b0cb..00000000000 --- a/apps/desktop/desktop_native/napi/src/passwords.rs +++ /dev/null @@ -1,46 +0,0 @@ -#[napi] -pub mod passwords { - - /// The error message returned when a password is not found during retrieval or deletion. - #[napi] - pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND; - - /// Fetch the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn get_password(service: String, account: String) -> napi::Result { - desktop_core::password::get_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Save the password to the keychain. Adds an entry if none exists otherwise updates the - /// existing entry. - #[napi] - pub async fn set_password( - service: String, - account: String, - password: String, - ) -> napi::Result<()> { - desktop_core::password::set_password(&service, &account, &password) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Delete the stored password from the keychain. - /// Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist. - #[napi] - pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - desktop_core::password::delete_password(&service, &account) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - /// Checks if the os secure storage is available - #[napi] - pub async fn is_available() -> napi::Result { - desktop_core::password::is_available() - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/powermonitors.rs b/apps/desktop/desktop_native/napi/src/powermonitors.rs deleted file mode 100644 index eb673bdbe68..00000000000 --- a/apps/desktop/desktop_native/napi/src/powermonitors.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[napi] -pub mod powermonitors { - use napi::{ - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - tokio, - }; - - #[napi] - pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> { - let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32); - desktop_core::powermonitor::on_lock(tx) - .await - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - tokio::spawn(async move { - while let Some(()) = rx.recv().await { - callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); - } - }); - Ok(()) - } - - #[napi] - pub async fn is_lock_monitor_available() -> napi::Result { - Ok(desktop_core::powermonitor::is_lock_monitor_available().await) - } -} diff --git a/apps/desktop/desktop_native/napi/src/processisolations.rs b/apps/desktop/desktop_native/napi/src/processisolations.rs deleted file mode 100644 index 6ab4a2a645d..00000000000 --- a/apps/desktop/desktop_native/napi/src/processisolations.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[napi] -pub mod processisolations { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn disable_coredumps() -> napi::Result<()> { - desktop_core::process_isolation::disable_coredumps() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn is_core_dumping_disabled() -> napi::Result { - desktop_core::process_isolation::is_core_dumping_disabled() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn isolate_process() -> napi::Result<()> { - desktop_core::process_isolation::isolate_process() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/sshagent.rs b/apps/desktop/desktop_native/napi/src/sshagent.rs deleted file mode 100644 index 83eec090302..00000000000 --- a/apps/desktop/desktop_native/napi/src/sshagent.rs +++ /dev/null @@ -1,163 +0,0 @@ -#[napi] -pub mod sshagent { - use std::sync::Arc; - - use napi::{ - bindgen_prelude::Promise, - threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, - }; - use tokio::{self, sync::Mutex}; - use tracing::error; - - #[napi] - pub struct SshAgentState { - state: desktop_core::ssh_agent::BitwardenDesktopAgent, - } - - #[napi(object)] - pub struct PrivateKey { - pub private_key: String, - pub name: String, - pub cipher_id: String, - } - - #[napi(object)] - pub struct SshKey { - pub private_key: String, - pub public_key: String, - pub key_fingerprint: String, - } - - #[napi(object)] - pub struct SshUIRequest { - pub cipher_id: Option, - pub is_list: bool, - pub process_name: String, - pub is_forwarding: bool, - pub namespace: Option, - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn serve( - callback: ThreadsafeFunction>, - ) -> napi::Result { - let (auth_request_tx, mut auth_request_rx) = - tokio::sync::mpsc::channel::(32); - let (auth_response_tx, auth_response_rx) = - tokio::sync::broadcast::channel::<(u32, bool)>(32); - let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); - // Wrap callback in Arc so it can be shared across spawned tasks - let callback = Arc::new(callback); - tokio::spawn(async move { - let _ = auth_response_rx; - - while let Some(request) = auth_request_rx.recv().await { - let cloned_response_tx_arc = auth_response_tx_arc.clone(); - let cloned_callback = callback.clone(); - tokio::spawn(async move { - let auth_response_tx_arc = cloned_response_tx_arc; - let callback = cloned_callback; - // In NAPI v3, obtain the JS callback return as a Promise and await it - // in Rust - let (tx, rx) = std::sync::mpsc::channel::>(); - let status = callback.call_with_return_value( - Ok(SshUIRequest { - cipher_id: request.cipher_id, - is_list: request.is_list, - process_name: request.process_name, - is_forwarding: request.is_forwarding, - namespace: request.namespace, - }), - ThreadsafeFunctionCallMode::Blocking, - move |ret: Result, napi::Error>, _env| { - if let Ok(p) = ret { - let _ = tx.send(p); - } - Ok(()) - }, - ); - - let result = if status == napi::Status::Ok { - match rx.recv() { - Ok(promise) => match promise.await { - Ok(v) => v, - Err(e) => { - error!(error = %e, "UI callback promise rejected"); - false - } - }, - Err(e) => { - error!(error = %e, "Failed to receive UI callback promise"); - false - } - } - } else { - error!(error = ?status, "Calling UI callback failed"); - false - }; - - let _ = auth_response_tx_arc - .lock() - .await - .send((request.request_id, result)) - .expect("should be able to send auth response to agent"); - }); - } - }); - - match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server( - auth_request_tx, - Arc::new(Mutex::new(auth_response_rx)), - ) { - Ok(state) => Ok(SshAgentState { state }), - Err(e) => Err(napi::Error::from_reason(e.to_string())), - } - } - - #[napi] - pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state.stop(); - Ok(()) - } - - #[napi] - pub fn is_running(agent_state: &mut SshAgentState) -> bool { - let bitwarden_agent_state = agent_state.state.clone(); - bitwarden_agent_state.is_running() - } - - #[napi] - pub fn set_keys( - agent_state: &mut SshAgentState, - new_keys: Vec, - ) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .set_keys( - new_keys - .iter() - .map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone())) - .collect(), - ) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - Ok(()) - } - - #[napi] - pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .lock() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[napi] - pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { - let bitwarden_agent_state = &mut agent_state.state; - bitwarden_agent_state - .clear_keys() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/desktop_native/napi/src/windows_registry.rs b/apps/desktop/desktop_native/napi/src/windows_registry.rs deleted file mode 100644 index e22e2ce46f5..00000000000 --- a/apps/desktop/desktop_native/napi/src/windows_registry.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[napi] -pub mod windows_registry { - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> { - crate::registry::create_key(&key, &subkey, &value) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - - #[allow(clippy::unused_async)] // FIXME: Remove unused async! - #[napi] - pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> { - crate::registry::delete_key(&key, &subkey) - .map_err(|e| napi::Error::from_reason(e.to_string())) - } -} diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 9c66b17aa1f..88ab1602dbe 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -32,6 +32,7 @@ "win": { "electronUpdaterCompatibility": ">=0.0.1", "target": ["portable", "nsis-web", "appx"], + "signExts": [".dll", ".node"], "signtoolOptions": { "sign": "./sign.js" }, @@ -61,7 +62,6 @@ "appx": { "artifactName": "Bitwarden-Beta-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "BitwardenBeta", "identityName": "8bitSolutionsLLC.BitwardenBeta", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 151ce72182d..9d21c04c122 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -93,6 +93,7 @@ "win": { "electronUpdaterCompatibility": ">=0.0.1", "target": ["portable", "nsis-web", "appx"], + "signExts": [".dll", ".node"], "signtoolOptions": { "sign": "./sign.js" }, @@ -176,7 +177,6 @@ "appx": { "artifactName": "${productName}-${version}-${arch}.${ext}", "backgroundColor": "#175DDC", - "customManifestPath": "./custom-appx-manifest.xml", "applicationId": "bitwardendesktop", "identityName": "8bitSolutionsLLC.bitwardendesktop", "publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd2147d21e4..5718c752a7c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 0f6e3ea370d..b708c07206b 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -53,4 +53,8 @@ modules: - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" - export ZYPAK_LD_PRELOAD="/app/bin/libprocess_isolation.so" - export PROCESS_ISOLATION_LD_PRELOAD="/app/bin/libprocess_isolation.so" - - exec zypak-wrapper /app/bin/bitwarden-app "$@" + - PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto" + - if [ "$USE_X11" != "false" ]; then + - PARAMS="--ozone-platform=x11" + - fi + - exec zypak-wrapper /app/bin/bitwarden-app "$@" "$PARAMS" diff --git a/apps/desktop/scripts/appx-cross-build.ps1 b/apps/desktop/scripts/appx-cross-build.ps1 index ef2ab09104c..c47567695ed 100755 --- a/apps/desktop/scripts/appx-cross-build.ps1 +++ b/apps/desktop/scripts/appx-cross-build.ps1 @@ -72,6 +72,7 @@ param( # Whether to build in release mode. $Release=$false ) + $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true $startTime = Get-Date @@ -113,7 +114,7 @@ else { $builderConfig = Get-Content $electronConfigFile | ConvertFrom-Json $packageConfig = Get-Content package.json | ConvertFrom-Json -$manifestTemplate = Get-Content $builderConfig.appx.customManifestPath +$manifestTemplate = Get-Content ($builderConfig.appx.customManifestPath ?? "custom-appx-manifest.xml") $srcDir = Get-Location $assetsDir = Get-Item $builderConfig.directories.buildResources diff --git a/apps/desktop/sign.js b/apps/desktop/sign.js index a01388c703c..c5a05df8480 100644 --- a/apps/desktop/sign.js +++ b/apps/desktop/sign.js @@ -3,7 +3,7 @@ const child_process = require("child_process"); exports.default = async function (configuration) { const ext = configuration.path.split(".").at(-1); - if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && ["exe"].includes(ext)) { + if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && ["exe", "dll", "node"].includes(ext)) { console.log(`[*] Signing file: ${configuration.path}`); child_process.execFileSync( "azuresigntool", @@ -25,7 +25,10 @@ exports.default = async function (configuration) { stdio: "inherit", }, ); - } else if (process.env.ELECTRON_BUILDER_SIGN_CERT && ["exe", "appx"].includes(ext)) { + } else if ( + process.env.ELECTRON_BUILDER_SIGN_CERT && + ["exe", "dll", "node", "appx"].includes(ext) + ) { console.log(`[*] Signing file: ${configuration.path}`); if (process.platform !== "win32") { console.warn( diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index e9b6dfdc9e5..5912ef21cb8 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -54,7 +54,7 @@ import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component"; import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component"; import { VaultV2Component } from "../vault/app/vault/vault-v2.component"; -import { VaultComponent } from "../vault/app/vault-v3/vault.component"; +import { VaultWrapperComponent } from "../vault/app/vault-v3/vault-wrapper.component"; import { DesktopLayoutComponent } from "./layout/desktop-layout.component"; import { SendComponent } from "./tools/send/send.component"; @@ -358,7 +358,8 @@ const routes: Routes = [ children: [ { path: "new-vault", - component: VaultComponent, + component: VaultWrapperComponent, + data: { pageTitle: { key: "vault" } } satisfies RouteDataProperties, }, { path: "new-sends", diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 430870a247b..6ceb2871b3f 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -7,6 +7,7 @@ import { InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { @@ -30,6 +31,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { newGuid } from "@bitwarden/guid"; import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; import { DesktopSetInitialPasswordService } from "./desktop-set-initial-password.service"; @@ -224,4 +226,68 @@ describe("DesktopSetInitialPasswordService", () => { superSpy.mockRestore(); }); }); + + describe("setInitialPasswordTdeUserWithPermission()", () => { + let credentials: SetInitialPasswordTdeUserWithPermissionCredentials; + let userId: UserId; + let superSpy: jest.SpyInstance; + + beforeEach(() => { + credentials = { + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + kdfConfig: DEFAULT_KDF_CONFIG, + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + }; + userId = newGuid() as UserId; + + superSpy = jest + .spyOn( + DefaultSetInitialPasswordService.prototype, + "setInitialPasswordTdeUserWithPermission", + ) + .mockResolvedValue(undefined); // undefined = successful + }); + + afterEach(() => { + superSpy.mockRestore(); + }); + + it("should call the setInitialPasswordTdeUserWithPermission() method on the default service", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(superSpy).toHaveBeenCalledWith(credentials, userId); + }); + + describe("given the initial password was successfully set", () => { + it("should send a 'redrawMenu' message", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("redrawMenu"); + }); + }); + + describe("given the initial password was NOT successfully set (due an error on the default service)", () => { + it("should NOT send a 'redrawMenu' message", async () => { + // Arrange + const error = new Error("error on DefaultSetInitialPasswordService"); + superSpy.mockRejectedValue(error); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow(error); + expect(messagingService.send).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts index 3b1562075f9..b03d87870f9 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.ts @@ -4,6 +4,7 @@ import { InitializeJitPasswordCredentials, SetInitialPasswordCredentials, SetInitialPasswordService, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -75,4 +76,13 @@ export class DesktopSetInitialPasswordService this.messagingService.send("redrawMenu"); } + + override async setInitialPasswordTdeUserWithPermission( + credentials: SetInitialPasswordTdeUserWithPermissionCredentials, + userId: UserId, + ) { + await super.setInitialPasswordTdeUserWithPermission(credentials, userId); + + this.messagingService.send("redrawMenu"); + } } diff --git a/apps/desktop/src/app/shared/shared.module.ts b/apps/desktop/src/app/shared/shared.module.ts index 85b3b800e83..90da5eba7ca 100644 --- a/apps/desktop/src/app/shared/shared.module.ts +++ b/apps/desktop/src/app/shared/shared.module.ts @@ -12,6 +12,12 @@ import { IconModule } from "@bitwarden/components"; import { AvatarComponent } from "../components/avatar.component"; import { ServicesModule } from "../services/services.module"; +/** + * @deprecated Please directly import the relevant directive/pipe/component. + * + * This module is overly large and adds many unrelated modules to your dependency tree. + * https://angular.dev/guide/ngmodules/overview recommends not using `NgModule`s for new code. + */ @NgModule({ imports: [ CommonModule, diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.html b/apps/desktop/src/app/tools/generator/credential-generator.component.html index 12088a147c5..241d21b1bb7 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.html +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.html @@ -5,14 +5,17 @@ diff --git a/apps/desktop/src/app/tools/generator/credential-generator.component.ts b/apps/desktop/src/app/tools/generator/credential-generator.component.ts index 42313c48f7f..036a5e104aa 100644 --- a/apps/desktop/src/app/tools/generator/credential-generator.component.ts +++ b/apps/desktop/src/app/tools/generator/credential-generator.component.ts @@ -1,13 +1,7 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - ButtonModule, - DialogModule, - DialogService, - ItemModule, - LinkModule, -} from "@bitwarden/components"; +import { ButtonModule, DialogModule, DialogService, ItemModule } from "@bitwarden/components"; import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, @@ -18,7 +12,7 @@ import { @Component({ selector: "credential-generator", templateUrl: "credential-generator.component.html", - imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule, LinkModule], + imports: [DialogModule, ButtonModule, JslibModule, GeneratorModule, ItemModule], }) export class CredentialGeneratorComponent { constructor(private dialogService: DialogService) {} diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts index 271418ae5b2..fc058c1a17f 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, computed, inject, signal, viewChild } from "@angular/core"; +import { Component, computed, DestroyRef, inject, signal, viewChild } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, map, switchMap, lastValueFrom } from "rxjs"; @@ -20,7 +20,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SendId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; -import { ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { ButtonModule, DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { NewSendDropdownV2Component, SendItemsService, @@ -28,6 +28,7 @@ import { SendListState, SendAddEditDialogComponent, DefaultSendFormConfigService, + SendItemDialogResult, } from "@bitwarden/send-ui"; import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; @@ -84,6 +85,9 @@ export class SendV2Component { private dialogService = inject(DialogService); private toastService = inject(ToastService); private logService = inject(LogService); + private destroyRef = inject(DestroyRef); + + private activeDrawerRef?: DialogRef; protected readonly useDrawerEditMode = toSignal( this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone2), @@ -128,6 +132,12 @@ export class SendV2Component { { initialValue: null }, ); + constructor() { + this.destroyRef.onDestroy(() => { + this.activeDrawerRef?.close(); + }); + } + protected readonly selectedSendType = computed(() => { const action = this.action(); @@ -143,11 +153,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { this.action.set(Action.Add); this.sendId.set(null); @@ -173,11 +184,12 @@ export class SendV2Component { if (this.useDrawerEditMode()) { const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId as SendId); - const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + this.activeDrawerRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig, }); - await lastValueFrom(dialogRef.closed); + await lastValueFrom(this.activeDrawerRef.closed); + this.activeDrawerRef = null; } else { if (sendId === this.sendId() && this.action() === Action.Edit) { return; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts index d18fb6752e3..87452664729 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-create.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; @@ -163,9 +165,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy { return; } - const userHandle = Fido2Utils.bufferToString( - new Uint8Array(lastRegistrationRequest.userHandle), - ); + const userHandle = Fido2Utils.arrayToString(new Uint8Array(lastRegistrationRequest.userHandle)); this.ciphers$ = combineLatest([ this.accountService.activeAccount$.pipe(map((a) => a?.id)), diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts index 274956be0eb..b8aecafa92b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-excluded-ciphers.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts index 70ef4461f6a..2a5da1b766b 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.spec.ts @@ -10,6 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service"; @@ -65,7 +66,9 @@ describe("Fido2VaultComponent", () => { { provide: I18nService, useValue: mockI18nService }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideProvider(DialogService, { useValue: mock() }) + .compileComponents(); fixture = TestBed.createComponent(Fido2VaultComponent); component = fixture.componentInstance; diff --git a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts index 635ba3972cb..588f52ef6d3 100644 --- a/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts +++ b/apps/desktop/src/autofill/modal/credentials/fido2-vault.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core"; import { RouterModule, Router } from "@angular/router"; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index cca0097d65e..b84720bb3c1 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Injectable, OnDestroy } from "@angular/core"; import { Subject, @@ -51,7 +53,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service" export class DesktopAutofillService implements OnDestroy { private destroy$ = new Subject(); private registrationRequest: autofill.PasskeyRegistrationRequest; - private featureFlag?: FeatureFlag; + private featureFlag?: typeof FeatureFlag.MacOsNativeCredentialSync; private isEnabled: boolean = false; constructor( @@ -378,13 +380,13 @@ export class DesktopAutofillService implements OnDestroy { if ("credentialId" in request) { allowedCredentials = [ { - id: new Uint8Array(request.credentialId).buffer, + id: new Uint8Array(request.credentialId), type: "public-key" as const, }, ]; } else { allowedCredentials = request.allowedCredentials.map((credentialId) => ({ - id: new Uint8Array(credentialId).buffer, + id: new Uint8Array(credentialId), type: "public-key" as const, })); } diff --git a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts index 432448faba3..050332349a1 100644 --- a/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts +++ b/apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Router } from "@angular/router"; import { lastValueFrom, diff --git a/apps/desktop/src/entry.ts b/apps/desktop/src/entry.ts index 9f03a84e627..65b655c7478 100644 --- a/apps/desktop/src/entry.ts +++ b/apps/desktop/src/entry.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { spawn } from "child_process"; import * as path from "path"; diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ef221f96878..dfcdd36da20 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Gaan Voort" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index bcac0529a8c..1273058bfc9 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "متابعة" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f94ff2417cf..ba43fecfc60 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Davam" }, @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { @@ -4487,7 +4490,7 @@ "message": "Vaxt bitmə əməliyyatı" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Xəta: Şifrəsi açıla bilmir" }, "sessionTimeoutHeader": { "message": "Sessiya vaxt bitməsi" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 3467fe20ae8..c554352c438 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Працягнуць" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index cab21191e37..6913b3b563e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "continue": { "message": "Продължаване" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "archiveItem": { "message": "Архивиране на елемента" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" + }, + "userVerificationFailed": { + "message": "Проверката на потребителя беше неуспешна." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 544d88a72a6..2919e52b0fb 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "অবিরত" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 289554a237f..61bb17d5171 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neispravan verifikacijski kod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 7b8d32a798c..ac59b1bd040 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continua" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 478343b7e7d..e5d009cbe2c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mail nebo ověřovací kód" + }, "continue": { "message": "Pokračovat" }, @@ -1055,7 +1058,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "uživatel@bitwarden.com, uživatel@společnost.cz" + }, + "userVerificationFailed": { + "message": "Ověření uživatele se nezdařilo." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 4feb0181431..6f39024dd17 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bd6a6f4379a..f7f6dd31da5 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsæt" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 205c8e95435..6d7f8843a32 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "E-Mail oder Verifizierungscode ungültig" + }, "continue": { "message": "Weiter" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen sowie Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde archiviert" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "archiveItem": { "message": "Eintrag archivieren" @@ -4487,7 +4490,7 @@ "message": "Timeout-Aktion" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "sessionTimeoutHeader": { "message": "Sitzungs-Timeout" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" + }, + "userVerificationFailed": { + "message": "Benutzerverifizierung fehlgeschlagen." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 97371668dca..fe21423ceba 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Συνέχεια" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f444265877d..44309929419 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -11,6 +11,9 @@ "favorites": { "message": "Favorites" }, + "unfavorite": { + "message": "Unfavorite" + }, "types": { "message": "Types" }, @@ -586,6 +589,12 @@ "editedItem": { "message": "Item saved" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, "deleteItem": { "message": "Delete item" }, @@ -1023,6 +1032,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -1552,6 +1564,18 @@ "unknown": { "message": "Unknown" }, + "copyAddress": { + "message": "Copy address" + }, + "copyPhone": { + "message": "Copy phone" + }, + "copyNote": { + "message": "Copy note" + }, + "copyVerificationCode": { + "message": "Copy verification code" + }, "copyUsername": { "message": "Copy username" }, @@ -2079,6 +2103,24 @@ "searchTrash": { "message": "Search trash" }, + "searchArchive": { + "message": "Search archive" + }, + "searchLogin": { + "message": "Search login" + }, + "searchCard": { + "message": "Search card" + }, + "searchIdentity": { + "message": "Search identity" + }, + "searchSecureNote": { + "message": "Search secure note" + }, + "searchSshKey": { + "message": "Search SSH key" + }, "permanentlyDeleteItem": { "message": "Permanently delete item" }, @@ -2375,6 +2417,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "me": { + "message": "Me" + }, "myVault": { "message": "My vault" }, @@ -4083,6 +4128,9 @@ "missingWebsite": { "message": "Missing website" }, + "missingPermissions": { + "message": "You lack the necessary permissions to perform this action." + }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", "placeholders": { @@ -4587,6 +4635,36 @@ "whyAmISeeingThis": { "message": "Why am I seeing this?" }, + "organizationIsSuspended": { + "message": "Organization is suspended" + }, + "organizationIsSuspendedDesc": { + "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "noItemsInVault": { + "message": "No items in the vault" + }, + "emptyVaultDescription": { + "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." + }, + "emptyFavorites": { + "message": "You haven't favorited any items" + }, + "emptyFavoritesDesc": { + "message": "Add frequently used items to favorites for quick access." + }, + "noSearchResults": { + "message": "No search results returned" + }, + "clearFiltersOrTryAnother": { + "message": "Clear filters or try another search term" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4615,7 +4693,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 22b482ed04d..04684ffe9bd 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 42a63ff0db1..bda8ffa8fd5 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index ae37e15d84c..79e1ece499d 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevalida kontrola kodo" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Daŭrigi" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d6210f940b5..91ec21c717f 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -773,7 +773,7 @@ "message": "Añadir adjunto" }, "itemsTransferred": { - "message": "Items transferred" + "message": "Elementos transferidos" }, "fixEncryption": { "message": "Corregir cifrado" @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificación incorrecto" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuar" }, @@ -2089,7 +2092,7 @@ "message": "Elemento eliminado de forma permanente" }, "archivedItemRestored": { - "message": "Archived item restored" + "message": "Elemento archivado restaurado" }, "restoredItem": { "message": "Elemento restaurado" @@ -4009,7 +4012,7 @@ "message": "No, no lo tengo" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sí, puedo acceder a mi correo electrónico de forma fiable" }, "turnOnTwoStepLogin": { "message": "Turn on two-step login" @@ -4075,10 +4078,10 @@ "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, "vulnerablePassword": { - "message": "Vulnerable password." + "message": "Contraseña vulnerable." }, "changeNow": { - "message": "Change now" + "message": "Cambiar ahora" }, "missingWebsite": { "message": "Missing website" @@ -4109,7 +4112,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsTitleNoSearchResults": { - "message": "No search results returned" + "message": "Ningún resultado de búsqueda devuelto" }, "sendsBodyNoItems": { "message": "Comparte archivos y datos de forma segura con cualquiera, en cualquier plataforma. Tu información permanecerá encriptada de extremo a extremo, limitando su exposición.", @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Los elementos archivados aparecerán aquí y se excluirán de los resultados de búsqueda generales y de sugerencias de autocompletado." }, - "itemWasSentToArchive": { - "message": "El elemento fue archivado" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "El elemento fue desarchivado" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archivar elemento" @@ -4403,7 +4406,7 @@ "message": "Desarchivar y guardar" }, "restartPremium": { - "message": "Restart Premium" + "message": "Reiniciar Premium" }, "premiumSubscriptionEnded": { "message": "Tu suscripción Premium ha terminado" @@ -4537,10 +4540,10 @@ "message": "Set an unlock method to change your timeout action" }, "upgrade": { - "message": "Upgrade" + "message": "Actualizar" }, "leaveConfirmationDialogTitle": { - "message": "Are you sure you want to leave?" + "message": "¿Estás seguro de que quieres salir?" }, "leaveConfirmationDialogContentOne": { "message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features." @@ -4558,10 +4561,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "¿Cómo gestiono mi caja fuerte?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Transferir elementos a $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -4579,13 +4582,13 @@ } }, "acceptTransfer": { - "message": "Accept transfer" + "message": "Aceptar transferencia" }, "declineAndLeave": { - "message": "Decline and leave" + "message": "Rechazar y salir" }, "whyAmISeeingThis": { - "message": "Why am I seeing this?" + "message": "¿Por qué estoy viendo esto?" }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", @@ -4595,27 +4598,33 @@ "message": "Email protected" }, "emails": { - "message": "Emails" + "message": "Correos electrónicos" }, "noAuth": { - "message": "Anyone with the link" + "message": "Cualquiera con el enlace" }, "anyOneWithPassword": { "message": "Anyone with a password set by you" }, "whoCanView": { - "message": "Who can view" + "message": "Quién puede ver" }, "specificPeople": { - "message": "Specific people" + "message": "Personas específicas" }, "emailVerificationDesc": { "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Introduce varios correos electrónicos separándolos con una coma." + }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 8c9476cc69e..84f432ac410 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jätka" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 75c33286fb7..2adb34fa9f2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jarraitu" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 4136edbde06..94e5a54ab4b 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ادامه" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "آیتم‌های بایگانی‌شده در اینجا نمایش داده می‌شوند و از نتایج جستجوی عمومی و پیشنهاد ها پر کردن خودکار حذف خواهند شد." }, - "itemWasSentToArchive": { - "message": "آیتم به بایگانی فرستاده شد" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "آیتم از بایگانی خارج شد" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "بایگانی آیتم" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b04b741bd3b..78eedc7e1ce 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Jatka" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index c503efc39f9..25dd12dd51e 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Magpatuloy" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index f04807aaeb9..04e0725c9b4 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuer" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiver l'élément" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 76904276732..3b80024392e 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index dbb2533e03e..66654aafbc6 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "המשך" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "העבר פריט לארכיון" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 1bfc0674ffe..d797b6319c0 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5ef663ab52b..969a0df9cee 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arhiviraj stavku" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5440b53e93d..d73d746fded 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "continue": { "message": "Folytatás" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { - "message": "Az elem visszavéelre került az archivumból." + "itemUnarchivedToast": { + "message": "Az elem visszavételre került az archivumból." }, "archiveItem": { "message": "Elem archiválása" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "A felhasználó ellenőrzése sikertelen volt." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index f4de0deff33..f5c74c471de 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Lanjutkan" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 9cd4783407d..ba1bb9e5346 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Codice di verifica non valido" + }, "continue": { "message": "Continua" }, @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dal riempimento automatico." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Elemento archiviato" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Elemento estratto dall'archivio" }, "archiveItem": { @@ -4487,7 +4490,7 @@ "message": "Azione al timeout" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Errore: impossibile decrittare" }, "sessionTimeoutHeader": { "message": "Timeout della sessione" @@ -4588,34 +4591,40 @@ "message": "Perché vedo questo avviso?" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "I destinatari dovranno inserire la password per visualizzare questo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "Email protected" + "message": "Email protetta" }, "emails": { - "message": "Emails" + "message": "Indirizzi email" }, "noAuth": { - "message": "Anyone with the link" + "message": "Chiunque abbia il link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Chiunque abbia una password impostata da te" }, "whoCanView": { - "message": "Who can view" + "message": "Chi può visualizzare" }, "specificPeople": { - "message": "Specific people" + "message": "Persone specifiche" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "I destinatari dovranno verificare il loro indirizzo email con un codice per poter visualizzare il Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Inserisci più indirizzi email separandoli con virgole." + }, + "emailsRequiredChangeAccessType": { + "message": "La verifica via email richiede almeno un indirizzo email. Per rimuovere tutte le email, modifica il tipo di accesso qui sopra." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Verifica dell'utente non riuscita." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1d46532a980..908fa271a16 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "続行" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 4618fd024a9..b9fb3f528d7 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "არასწორი გადამოწმების კოდი" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "გაგრძელება" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 76904276732..3b80024392e 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 3bb7f513701..13463f63da1 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ಮುಂದುವರಿಸಿ" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 70ffd234941..524d1bc8b01 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "계속" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index f703b1f5d53..3a003cb565e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tęsti" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index e82a4b91487..b8a0cdd8434 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "continue": { "message": "Turpināt" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Vienums ievietots arhīvā" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Vienums izņemts no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Lietotāja apliecināšana neizdevās." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 25bb0cbc816..de1c690bb8e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nastavi" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 43e0dc85fb0..0b15dafd31b 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "തുടരുക" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 76904276732..3b80024392e 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index ddc8bef0241..6efe4072ecb 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 792c95eb1ec..a4842046c16 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig verifiseringskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 89c3d3ba231..43a96999ed3 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index e4b1d6d8abc..f6908fd6498 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "continue": { "message": "Doorgaan" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "archiveItem": { "message": "Item archiveren" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Gebruikersverificatie is mislukt." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 850696f8fcf..1c0ab7c51a8 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ugyldig stadfestingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsett" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index b63a970e9c2..f28ba8ad5a0 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index e2d9fbce4a2..e429217c278 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Kontynuuj" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Zarchiwizowane elementy pojawią się tutaj i zostaną wykluczone z wyników wyszukiwania i sugestii autouzupełniania." }, - "itemWasSentToArchive": { - "message": "Element został przeniesiony do archiwum" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Element został usunięty z archiwum" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archiwizuj element" @@ -4487,7 +4490,7 @@ "message": "Timeout action" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Błąd: Nie można odszyfrować" }, "sessionTimeoutHeader": { "message": "Session timeout" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 3a055bdb03d..1ec4807e2a4 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -4487,7 +4490,7 @@ "message": "Ação do limite de tempo" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "sessionTimeoutHeader": { "message": "Limite de tempo da sessão" @@ -4588,11 +4591,11 @@ "message": "Por que estou vendo isso?" }, "sendPasswordHelperText": { - "message": "Indivíduos precisarão utilizar a senha para ver este Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "emails": { "message": "E-mails" @@ -4601,7 +4604,7 @@ "message": "Qualquer um com o link" }, "anyOneWithPassword": { - "message": "Qualquer um com uma senha definida por você" + "message": "Qualquer pessoa com uma senha configurada por você" }, "whoCanView": { "message": "Quem pode visualizar" @@ -4613,9 +4616,15 @@ "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Insira múltiplos e-mails separando-os com vírgula." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { - "message": "user@bitwarden.com , user@acme.com" + "message": "usuário@bitwarden.com , usuário@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do usuário." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index bb99678bde6..ca5091ccbe6 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "continue": { "message": "Continuar" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "archiveItem": { "message": "Arquivar item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" + }, + "userVerificationFailed": { + "message": "Falha na verificação do utilizador." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 6b51cb9fecd..6c67e9d9e06 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continuare" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 389f4b37dfd..e48319dac55 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "continue": { "message": "Продолжить" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "archiveItem": { "message": "Архивировать элемент" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "Проверка пользователя не удалась." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 9e8a727ad5d..4d75c329656 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ඉදිරියට" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 471984785d8..50d88a49405 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Pokračovať" }, @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { + "itemArchiveToast": { "message": "Položka bola archivovaná" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadajte viacero e-mailových adries oddelených čiarkou." }, + "emailsRequiredChangeAccessType": { + "message": "Overenie e-mailu vyžaduje aspoň jednu e-mailovú adresu. Ak chcete odstrániť všetky e-maily, zmeňte typ prístupu vyššie." + }, "emailPlaceholder": { "message": "pouzivate@bitwarden.com, pouzivatel@acme.com" + }, + "userVerificationFailed": { + "message": "Overenie používateľa zlyhalo." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 23d9f18fadb..63ae05a12b5 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Neveljavna verifikacijska koda" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Nadaljuj" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 1a68810bca3..32715ed60d1 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Настави" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архивирај ставку" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 1dd66c409c0..b44d54a14a3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Fortsätt" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt kommer att visas här och kommer att uteslutas från allmänna sökresultat och förslag för autofyll." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Arkivera objekt" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" + }, + "userVerificationFailed": { + "message": "Verifiering av användare misslyckades." } } diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 3a7fc795668..feb4b92c77e 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "தொடரவும்" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 76904276732..3b80024392e 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Continue" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index bcff8d0849e..eeb0029b928 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "รหัสการตรวจสอบสิทธิ์ไม่ถูกต้อง" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "ดำเนินการต่อไป" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Archive item" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 936a68516bd..29171ca8fef 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "continue": { "message": "Devam Et" }, @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünecek ve genel arama sonuçları ile otomatik doldurma önerilerinden hariç tutulacaktır." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" + }, + "userVerificationFailed": { + "message": "Kullanıcı doğrulaması başarısız oldu." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 2b4cfcb7c22..e982d46b454 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Продовжити" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { - "message": "Запис архівовано" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Запис розархівовано" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Архівувати запис" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 56c9d9a5c6e..8fe0adee948 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "Tiếp tục" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "Lưu trữ mục" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index cf42adba294..bd3a897ffab 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "无效的电子邮箱或验证码" + }, "continue": { "message": "继续" }, @@ -4387,10 +4390,10 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "项目已归档" }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "项目已取消归档" }, "archiveItem": { @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" + }, + "userVerificationFailed": { + "message": "用户验证失败。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 3157e929e8f..364f67c7b58 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1023,6 +1023,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "continue": { "message": "繼續" }, @@ -4387,11 +4390,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "archiveItem": { "message": "封存項目" @@ -4615,7 +4618,13 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" + }, + "userVerificationFailed": { + "message": "User verification failed." } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 63b288e9161..19f11576d64 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -103,11 +103,6 @@ export class Main { appDataPath = path.join(process.env.SNAP_USER_DATA, "appdata"); } - // Workaround for bug described here: https://github.com/electron/electron/issues/46538 - if (process.platform === "linux") { - app.commandLine.appendSwitch("gtk-version", "3"); - } - app.on("ready", () => { // on ready stuff... }); diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index 60b4f282405..55f57c339fa 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { dialog, shell, Notification } from "electron"; import log from "electron-log"; import { autoUpdater, UpdateDownloadedEvent, VerifyUpdateSupport } from "electron-updater"; diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 2872154aa44..b4ced4471fa 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -4,7 +4,7 @@ import { once } from "node:events"; import * as path from "path"; import * as url from "url"; -import { app, BrowserWindow, dialog, ipcMain, nativeTheme, screen, session } from "electron"; +import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron"; import { concatMap, firstValueFrom, pairwise } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -127,7 +127,6 @@ export class WindowMain { if (!isMacAppStore()) { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { - dialog.showErrorBox("Error", "An instance of Bitwarden Desktop is already running."); app.quit(); return; } else { diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0aa188eba2f..01c429ab3d0 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2026.2.0", + "version": "2026.2.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 0076981ab60..fac797b5344 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2026.2.0", + "version": "2026.2.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/scss/base.scss b/apps/desktop/src/scss/base.scss index a95d82dacd4..18e782d341f 100644 --- a/apps/desktop/src/scss/base.scss +++ b/apps/desktop/src/scss/base.scss @@ -51,7 +51,7 @@ img { border: none; } -a { +a:not([bitlink]) { text-decoration: none; @include themify($themes) { @@ -102,23 +102,30 @@ textarea { div:not(.modal)::-webkit-scrollbar, .cdk-virtual-scroll-viewport::-webkit-scrollbar, -.vault-filters::-webkit-scrollbar { +.vault-filters::-webkit-scrollbar, +#bit-side-nav::-webkit-scrollbar { width: 10px; height: 10px; } div:not(.modal)::-webkit-scrollbar-track, .cdk-virtual-scroll-viewport::-webkit-scrollbar-track, -.vault-filters::-webkit-scrollbar-track { +.vault-filters::-webkit-scrollbar-track, +#bit-side-nav::-webkit-scrollbar-track { background-color: transparent; } div:not(.modal)::-webkit-scrollbar-thumb, .cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, -.vault-filters::-webkit-scrollbar-thumb { +.vault-filters::-webkit-scrollbar-thumb, +#bit-side-nav::-webkit-scrollbar-thumb { border-radius: 10px; margin-right: 1px; +} +div:not(.modal)::-webkit-scrollbar-thumb, +.cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, +.vault-filters::-webkit-scrollbar-thumb { @include themify($themes) { background-color: themed("scrollbarColor"); } @@ -130,6 +137,18 @@ div:not(.modal)::-webkit-scrollbar-thumb, } } +#bit-side-nav::-webkit-scrollbar-thumb { + @include themify($themes) { + background-color: themed("scrollbarColorNav"); + } + + &:hover { + @include themify($themes) { + background-color: themed("scrollbarHoverColorNav"); + } + } +} + // cdk-virtual-scroll .cdk-virtual-scroll-viewport { width: 100%; diff --git a/apps/desktop/src/scss/environment.scss b/apps/desktop/src/scss/environment.scss index 699f2246b4a..e6ea95ef17e 100644 --- a/apps/desktop/src/scss/environment.scss +++ b/apps/desktop/src/scss/environment.scss @@ -11,6 +11,8 @@ .vault > .groupings > .content > .inner-content { padding-top: 0; } + + --bit-sidenav-macos-extra-top-padding: 28px; } .environment-selector-btn { diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index a00257ed608..62d4f23ad46 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -56,13 +56,15 @@ $themes: ( backgroundColorAlt2: $background-color-alt2, scrollbarColor: rgba(100, 100, 100, 0.2), scrollbarHoverColor: rgba(100, 100, 100, 0.4), + scrollbarColorNav: rgba(226, 226, 226), + scrollbarHoverColorNav: rgba(197, 197, 197), boxBackgroundColor: $box-background-color, boxBackgroundHoverColor: $box-background-hover-color, boxBorderColor: $box-border-color, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), - headerInputBackgroundColor: darken($brand-primary, 8%), - headerInputBackgroundFocusColor: darken($brand-primary, 10%), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), + headerInputBackgroundColor: darken($brand-primary, 20%), + headerInputBackgroundFocusColor: darken($brand-primary, 25%), headerInputColor: #ffffff, headerInputPlaceholderColor: lighten($brand-primary, 35%), listItemBackgroundColor: $background-color, @@ -115,11 +117,13 @@ $themes: ( backgroundColorAlt2: #15181e, scrollbarColor: #6e788a, scrollbarHoverColor: #8d94a5, + scrollbarColorNav: #6e788a, + scrollbarHoverColorNav: #8d94a5, boxBackgroundColor: #2f343d, boxBackgroundHoverColor: #3c424e, boxBorderColor: #4c525f, - headerBackgroundColor: rgb(var(--color-background-alt3)), - headerBorderColor: rgb(var(--color-background-alt4)), + headerBackgroundColor: var(--color-sidenav-background), + headerBorderColor: var(--color-sidenav-active-item), headerInputBackgroundColor: #3c424e, headerInputBackgroundFocusColor: #4c525f, headerInputColor: #ffffff, diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts index 520c29833e3..b5c39414eae 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts @@ -55,13 +55,6 @@ export class OrganizationFilterComponent { protected applyFilter(event: Event, organization: TreeNode) { event.stopPropagation(); - if (!organization.node.enabled) { - this.toastService.showToast({ - variant: "error", - message: this.i18nService.t("disabledOrganizationFilterError"), - }); - return; - } this.vaultFilterService.setOrganizationFilter(organization.node); const filter = this.activeFilter(); diff --git a/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.html b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.html new file mode 100644 index 00000000000..cf4bc6ceebd --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.html @@ -0,0 +1,195 @@ + + +
+ + @if (hasAttachments()) { + + {{ "attachments" | i18n }} + @if (showFixOldAttachments()) { + + {{ "attachmentsNeedFix" | i18n }} + } + } + {{ subtitle() }} +
+ +@if (showOwner()) { + + + + +} + + @if (decryptionFailure()) { + + + @if (canDeleteCipher()) { + + } + + } @else { + @if (canLaunch()) { + + } + @if (showCopyButton()) { + + + @for (copyField of copyFields(); track copyField.field) { + + } + + } + + + + @if (canLaunch()) { + + } + @for (copyField of copyFields(); track copyField.field) { + + } + @if (showMenuDivider()) { + + } + @if (showFavorite()) { + + } + @if (canEditCipher()) { + + } + @if (showAttachments()) { + + } + @if (showClone()) { + + } + @if (showAssignToCollections()) { + + } + @if (showArchiveButton()) { + @if (userCanArchive()) { + + } + @if (!userCanArchive()) { + + } + } + + @if (showUnArchiveButton()) { + + } + + @if (isDeleted() && canRestoreCipher()) { + + } + @if (canDeleteCipher()) { + + } + + } + diff --git a/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.ts new file mode 100644 index 00000000000..98b0db1f05e --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-cipher-row.component.ts @@ -0,0 +1,301 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { NgClass } from "@angular/common"; +import { Component, HostListener, ViewChild, computed, inject, input, output } from "@angular/core"; + +import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge/premium-badge.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { + CipherViewLike, + CipherViewLikeUtils, +} from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { + AriaDisableDirective, + BitIconButtonComponent, + MenuModule, + MenuTriggerForDirective, + TooltipDirective, + TableModule, +} from "@bitwarden/components"; +import { + CopyAction, + CopyCipherFieldDirective, + GetOrgNameFromIdPipe, + OrganizationNameBadgeComponent, +} from "@bitwarden/vault"; + +import { VaultItemEvent } from "./vault-item-event"; + +/** Configuration for a copyable field */ +interface CopyFieldConfig { + field: CopyAction; + title: string; +} + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "tr[appVaultCipherRow]", + templateUrl: "vault-cipher-row.component.html", + imports: [ + NgClass, + JslibModule, + TableModule, + AriaDisableDirective, + OrganizationNameBadgeComponent, + TooltipDirective, + BitIconButtonComponent, + MenuModule, + CopyCipherFieldDirective, + PremiumBadgeComponent, + GetOrgNameFromIdPipe, + ], +}) +export class VaultCipherRowComponent { + protected RowHeightClass = `tw-h-[75px]`; + + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective; + + protected readonly disabled = input(); + protected readonly cipher = input(); + protected readonly showOwner = input(); + protected readonly showPremiumFeatures = input(); + protected readonly useEvents = input(); + protected readonly cloneable = input(); + protected readonly organizations = input(); + protected readonly canEditCipher = input(); + protected readonly canAssignCollections = input(); + protected readonly canManageCollection = input(); + /** + * uses new permission delete logic from PM-15493 + */ + protected readonly canDeleteCipher = input(); + /** + * uses new permission restore logic from PM-15493 + */ + protected readonly canRestoreCipher = input(); + /** + * user has archive permissions + */ + protected readonly userCanArchive = input(); + /** Archive feature is enabled */ + readonly archiveEnabled = input.required(); + /** + * Enforce Org Data Ownership Policy Status + */ + protected readonly enforceOrgDataOwnershipPolicy = input(); + protected readonly onEvent = output>(); + + protected CipherType = CipherType; + + private platformUtilsService = inject(PlatformUtilsService); + + protected readonly showArchiveButton = computed(() => { + return ( + this.archiveEnabled() && + !this.cipher().organizationId && + !CipherViewLikeUtils.isArchived(this.cipher()) && + !CipherViewLikeUtils.isDeleted(this.cipher()) + ); + }); + + // If item is archived always show unarchive button, even if user is not premium + protected readonly showUnArchiveButton = computed(() => { + return ( + CipherViewLikeUtils.isArchived(this.cipher()) && !CipherViewLikeUtils.isDeleted(this.cipher()) + ); + }); + + protected readonly showFixOldAttachments = computed(() => { + return this.cipher().hasOldAttachments && this.cipher().organizationId == null; + }); + + protected readonly hasAttachments = computed(() => { + return CipherViewLikeUtils.hasAttachments(this.cipher()); + }); + + // Do not show attachments button if: + // item is archived AND user is not premium user + protected readonly showAttachments = computed(() => { + if (CipherViewLikeUtils.isArchived(this.cipher()) && !this.userCanArchive()) { + return false; + } + return this.canEditCipher() || this.hasAttachments(); + }); + + protected readonly canLaunch = computed(() => { + return CipherViewLikeUtils.canLaunch(this.cipher()); + }); + + protected handleLaunch() { + const launchUri = CipherViewLikeUtils.getLaunchUri(this.cipher()); + this.platformUtilsService.launchUri(launchUri); + } + + protected readonly subtitle = computed(() => { + return CipherViewLikeUtils.subtitle(this.cipher()); + }); + + protected readonly isDeleted = computed(() => { + return CipherViewLikeUtils.isDeleted(this.cipher()); + }); + + protected readonly decryptionFailure = computed(() => { + return CipherViewLikeUtils.decryptionFailure(this.cipher()); + }); + + protected readonly showFavorite = computed(() => { + if (CipherViewLikeUtils.isArchived(this.cipher()) && !this.userCanArchive()) { + return false; + } + return true; + }); + + // Do Not show Assign to Collections option if item is archived + protected readonly showAssignToCollections = computed(() => { + if (CipherViewLikeUtils.isArchived(this.cipher())) { + return false; + } + return ( + this.organizations()?.length && + this.canAssignCollections() && + !CipherViewLikeUtils.isDeleted(this.cipher()) + ); + }); + + // Do NOT show clone option if: + // item is archived AND user is not premium user + // item is archived AND enforce org data ownership policy is on + protected readonly showClone = computed(() => { + if ( + CipherViewLikeUtils.isArchived(this.cipher()) && + (!this.userCanArchive() || this.enforceOrgDataOwnershipPolicy()) + ) { + return false; + } + return this.cloneable() && !CipherViewLikeUtils.isDeleted(this.cipher()); + }); + + protected readonly showMenuDivider = computed(() => this.showCopyButton() || this.canLaunch()); + + /** + * Returns the list of copyable fields based on cipher type. + * Used to render copy menu items dynamically. + */ + protected readonly copyFields = computed((): CopyFieldConfig[] => { + const cipher = this.cipher(); + + // No copy options for deleted or archived items + if (this.isDeleted() || CipherViewLikeUtils.isArchived(cipher)) { + return []; + } + + const cipherType = CipherViewLikeUtils.getType(cipher); + + switch (cipherType) { + case CipherType.Login: { + const fields: CopyFieldConfig[] = [{ field: "username", title: "copyUsername" }]; + if (cipher.viewPassword) { + fields.push({ field: "password", title: "copyPassword" }); + } + if ( + CipherViewLikeUtils.getLogin(cipher).totp && + (cipher.organizationUseTotp || this.showPremiumFeatures()) + ) { + fields.push({ field: "totp", title: "copyVerificationCode" }); + } + return fields; + } + case CipherType.Card: + return [ + { field: "cardNumber", title: "copyNumber" }, + { field: "securityCode", title: "copySecurityCode" }, + ]; + case CipherType.Identity: + return [ + { field: "username", title: "copyUsername" }, + { field: "email", title: "copyEmail" }, + { field: "phone", title: "copyPhone" }, + { field: "address", title: "copyAddress" }, + ]; + case CipherType.SecureNote: + return [{ field: "secureNote", title: "copyNote" }]; + default: + return []; + } + }); + + /** + * Determines if the copy button should be shown. + * Returns true only if at least one field has a copyable value. + */ + protected readonly showCopyButton = computed(() => { + const cipher = this.cipher(); + return this.copyFields().some(({ field }) => + CipherViewLikeUtils.hasCopyableValue(cipher, field), + ); + }); + + protected clone() { + this.onEvent.emit({ type: "clone", item: this.cipher() }); + } + + protected events() { + this.onEvent.emit({ type: "viewEvents", item: this.cipher() }); + } + + protected archive() { + this.onEvent.emit({ type: "archive", items: [this.cipher()] }); + } + + protected unarchive() { + this.onEvent.emit({ type: "unarchive", items: [this.cipher()] }); + } + + protected restore() { + this.onEvent.emit({ type: "restore", items: [this.cipher()] }); + } + + protected deleteCipher() { + this.onEvent.emit({ type: "delete", items: [{ cipher: this.cipher() }] }); + } + + protected attachments() { + this.onEvent.emit({ type: "viewAttachments", item: this.cipher() }); + } + + protected assignToCollections() { + this.onEvent.emit({ type: "assignToCollections", items: [this.cipher()] }); + } + + protected toggleFavorite() { + this.onEvent.emit({ + type: "toggleFavorite", + item: this.cipher(), + }); + } + + protected editCipher() { + this.onEvent.emit({ type: "editCipher", item: this.cipher() }); + } + + protected viewCipher() { + this.onEvent.emit({ type: "viewCipher", item: this.cipher() }); + } + + @HostListener("contextmenu", ["$event"]) + protected onRightClick(event: MouseEvent) { + if (event.shiftKey && event.ctrlKey) { + return; + } + + if (!this.disabled() && this.menuTrigger) { + this.menuTrigger.toggleMenuOnRightClick(event); + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.html b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.html new file mode 100644 index 00000000000..bdcbeb63845 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.html @@ -0,0 +1,40 @@ + +
+ + +
+ +@if (showOwner()) { + + + + +} + diff --git a/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.ts new file mode 100644 index 00000000000..6d3544f43b5 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-collection-row.component.ts @@ -0,0 +1,38 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { NgClass } from "@angular/common"; +import { Component, input } from "@angular/core"; +import { RouterLink } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + CollectionView, + CollectionTypes, +} from "@bitwarden/common/admin-console/models/collections"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { TableModule } from "@bitwarden/components"; +import { GetOrgNameFromIdPipe, OrganizationNameBadgeComponent } from "@bitwarden/vault"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "tr[appVaultCollectionRow]", + templateUrl: "vault-collection-row.component.html", + imports: [ + TableModule, + NgClass, + JslibModule, + RouterLink, + OrganizationNameBadgeComponent, + GetOrgNameFromIdPipe, + ], +}) +export class VaultCollectionRowComponent { + protected RowHeightClass = `tw-h-[75px]`; + protected DefaultCollectionType = CollectionTypes.DefaultUserCollection; + + protected readonly disabled = input(); + protected readonly collection = input(); + protected readonly showOwner = input(); + protected readonly organizations = input(); +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-items/vault-item-event.ts b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-item-event.ts new file mode 100644 index 00000000000..c3a1af43d07 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-items/vault-item-event.ts @@ -0,0 +1,7 @@ +import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { VaultItemEvent as BaseVaultItemEvent } from "@bitwarden/vault"; + +// Extend base events with desktop-specific events +export type VaultItemEvent = + | BaseVaultItemEvent + | { type: "viewCipher"; item: C }; diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.html b/apps/desktop/src/vault/app/vault-v3/vault-list.component.html new file mode 100644 index 00000000000..8f84ec72a1f --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.html @@ -0,0 +1,103 @@ +@if (showPremiumCallout()) { +
+ + +
+ {{ "premiumSubscriptionEndedDesc" | i18n }} +
+ + {{ "restartPremium" | i18n }} + +
+
+
+} + +
+ + + + + + {{ "name" | i18n }} + + @if (showOwner()) { + + {{ "owner" | i18n }} + + } + + {{ "options" | i18n }} + + + + + + @if (item.collection) { + + } + @if (item.cipher) { + + } + + + +
+ @if (isEmpty()) { + +
+ {{ emptyStateItem()?.title | i18n }} +
+

+ {{ emptyStateItem()?.description | i18n }} +

+ @if (showAddCipherBtn()) { + + } +
+ } +
diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts new file mode 100644 index 00000000000..9767692b7a6 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts @@ -0,0 +1,212 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { AsyncPipe } from "@angular/common"; +import { Component, input, output, effect, inject, computed } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Observable, of, switchMap } from "rxjs"; + +import { BitSvg } from "@bitwarden/assets/svg"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { + CipherViewLike, + CipherViewLikeUtils, +} from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { + SortDirection, + TableDataSource, + TableModule, + MenuModule, + ButtonModule, + IconButtonModule, + NoItemsModule, + CalloutComponent, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { NewCipherMenuComponent, VaultItem } from "@bitwarden/vault"; + +import { VaultCipherRowComponent } from "./vault-items/vault-cipher-row.component"; +import { VaultCollectionRowComponent } from "./vault-items/vault-collection-row.component"; +import { VaultItemEvent } from "./vault-items/vault-item-event"; + +// Fixed manual row height required due to how cdk-virtual-scroll works +export const RowHeight = 75; +export const RowHeightClass = `tw-h-[75px]`; +type EmptyStateItem = { + title: string; + description: string; + icon: BitSvg; +}; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "app-vault-list", + templateUrl: "vault-list.component.html", + imports: [ + ScrollingModule, + TableModule, + I18nPipe, + AsyncPipe, + MenuModule, + ButtonModule, + IconButtonModule, + VaultCollectionRowComponent, + VaultCipherRowComponent, + NoItemsModule, + NewCipherMenuComponent, + CalloutComponent, + ], +}) +export class VaultListComponent { + protected RowHeight = RowHeight; + + protected readonly disabled = input(); + protected readonly showOwner = input(); + protected readonly showPremiumFeatures = input(); + protected readonly allOrganizations = input([]); + protected readonly allCollections = input([]); + protected readonly userCanArchive = input(); + protected readonly enforceOrgDataOwnershipPolicy = input(); + protected readonly placeholderText = input(""); + protected readonly ciphers = input([]); + protected readonly collections = input([]); + protected readonly isEmpty = input(); + protected readonly showAddCipherBtn = input(); + protected readonly emptyStateItem = input(); + readonly showPremiumCallout = input(false); + + protected onEvent = output>(); + protected onAddCipher = output(); + protected onAddFolder = output(); + + protected cipherAuthorizationService = inject(CipherAuthorizationService); + protected restrictedItemTypesService = inject(RestrictedItemTypesService); + protected cipherArchiveService = inject(CipherArchiveService); + private premiumUpgradePromptService = inject(PremiumUpgradePromptService); + + protected dataSource = new TableDataSource>(); + private restrictedTypes: RestrictedCipherType[] = []; + + protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$; + + constructor() { + this.restrictedItemTypesService.restricted$.pipe(takeUntilDestroyed()).subscribe((types) => { + this.restrictedTypes = types; + this.refreshItems(); + }); + + // Refresh items when collections or ciphers change + effect(() => { + this.collections(); + this.ciphers(); + this.refreshItems(); + }); + } + + protected readonly showExtraColumn = computed(() => this.showOwner()); + + protected event(event: VaultItemEvent) { + this.onEvent.emit(event); + } + + protected addCipher(type: CipherType) { + this.onAddCipher.emit(type); + } + + protected addFolder() { + this.onAddFolder.emit(); + } + + protected canClone$(vaultItem: VaultItem): Observable { + return this.restrictedItemTypesService.restricted$.pipe( + switchMap((restrictedTypes) => { + // This will check for restrictions from org policies before allowing cloning. + const isItemRestricted = restrictedTypes.some( + (rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher), + ); + if (isItemRestricted) { + return of(false); + } + return this.cipherAuthorizationService.canCloneCipher$(vaultItem.cipher); + }), + ); + } + + protected canEditCipher(cipher: C) { + if (cipher.organizationId == null) { + return true; + } + return cipher.edit; + } + + protected canAssignCollections(cipher: C) { + const editableCollections = this.allCollections().filter((c) => !c.readOnly); + return CipherViewLikeUtils.canAssignToCollections(cipher) && editableCollections.length > 0; + } + + protected canManageCollection(cipher: C) { + // If the cipher is not part of an organization (personal item), user can manage it + if (cipher.organizationId == null) { + return true; + } + + return this.allCollections() + .filter((c) => cipher.collectionIds.includes(c.id as any)) + .some((collection) => collection.manage); + } + + private refreshItems() { + const collections: VaultItem[] = + this.collections()?.map((collection) => ({ collection })) || []; + const ciphers: VaultItem[] = this.ciphers().map((cipher) => ({ cipher })); + const items: VaultItem[] = [].concat(collections).concat(ciphers); + + this.dataSource.data = items; + } + + /** + * Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name. + */ + protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + return this.compareNames(a, b); + }; + + protected sortByOwner = (a: VaultItem, b: VaultItem, direction: SortDirection) => { + const getOwnerName = (item: VaultItem): string => { + if (item.cipher) { + return (item.cipher.organizationId as string) || ""; + } else if (item.collection) { + return (item.collection.organizationId as string) || ""; + } + return ""; + }; + + const ownerA = getOwnerName(a); + const ownerB = getOwnerName(b); + + return ownerA.localeCompare(ownerB); + }; + + private compareNames(a: VaultItem, b: VaultItem): number { + const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name; + return getName(a)?.localeCompare(getName(b)) ?? -1; + } + + protected trackByFn(index: number, item: VaultItem) { + return item.cipher?.id || item.collection?.id || index; + } + + async navigateToGetPremium() { + await this.premiumUpgradePromptService.promptForPremium(); + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-orig.component.html b/apps/desktop/src/vault/app/vault-v3/vault-orig.component.html new file mode 100644 index 00000000000..5d8c3491710 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-orig.component.html @@ -0,0 +1,86 @@ +
+ + + @if (!!action) { +
+ +
+
+
+ @if (action === "view") { + + } + @if (action === "add" || action === "edit" || action === "clone") { + + + + + + } +
+
+
+
+ } + @if (!["add", "edit", "view", "clone"].includes(action)) { + + } +
+ diff --git a/apps/desktop/src/vault/app/vault-v3/vault-orig.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-orig.component.ts new file mode 100644 index 00000000000..b6bab6716fb --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-orig.component.ts @@ -0,0 +1,1043 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { CommonModule } from "@angular/common"; +import { + ChangeDetectorRef, + Component, + computed, + NgZone, + OnDestroy, + OnInit, + signal, + ViewChild, +} from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + combineLatest, + firstValueFrom, + Subject, + takeUntil, + switchMap, + lastValueFrom, + Observable, + from, +} from "rxjs"; +import { filter, map, take } from "rxjs/operators"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; +import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; +import { ItemTypes } from "@bitwarden/assets/svg"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { EventType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getByIds } from "@bitwarden/common/platform/misc"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType, toCipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + CipherViewLike, + CipherViewLikeUtils, +} from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; +import { + BadgeModule, + ButtonModule, + DialogService, + ItemModule, + ToastService, + CopyClickListener, + COPY_CLICK_LISTENER, + NoItemsModule, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + AttachmentDialogResult, + AttachmentsV2Component, + ChangeLoginPasswordService, + CipherFormConfig, + CipherFormConfigService, + CipherFormGenerationService, + CipherFormMode, + CipherFormModule, + CipherViewComponent, + CollectionAssignmentResult, + createFilterFunction, + DecryptionFailureDialogComponent, + DefaultChangeLoginPasswordService, + DefaultCipherFormConfigService, + PasswordRepromptService, + CipherFormComponent, + ArchiveCipherUtilitiesService, + VaultFilter, + VaultFilterServiceAbstraction as VaultFilterService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, + VaultItemsTransferService, + DefaultVaultItemsTransferService, +} from "@bitwarden/vault"; + +import { SearchBarService } from "../../../app/layout/search/search-bar.service"; +import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service"; +import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; +import { invokeMenu, RendererMenuItem } from "../../../utils"; +import { AssignCollectionsDesktopComponent } from "../vault/assign-collections"; +import { ItemFooterComponent } from "../vault/item-footer.component"; +import { VaultItemsV2Component } from "../vault/vault-items-v2.component"; + +const BroadcasterSubscriptionId = "VaultComponent"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "app-vault-v3", + templateUrl: "vault-orig.component.html", + imports: [ + BadgeModule, + CommonModule, + CipherFormModule, + CipherViewComponent, + ItemFooterComponent, + I18nPipe, + ItemModule, + ButtonModule, + PremiumBadgeComponent, + VaultItemsV2Component, + NoItemsModule, + ], + providers: [ + { + provide: CipherFormConfigService, + useClass: DefaultCipherFormConfigService, + }, + { + provide: ChangeLoginPasswordService, + useClass: DefaultChangeLoginPasswordService, + }, + { + provide: ViewPasswordHistoryService, + useClass: VaultViewPasswordHistoryService, + }, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, + { + provide: COPY_CLICK_LISTENER, + useExisting: VaultComponent, + }, + { provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService }, + ], +}) +export class VaultComponent implements OnInit, OnDestroy, CopyClickListener { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @ViewChild(VaultItemsV2Component, { static: true }) + vaultItemsComponent: VaultItemsV2Component | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @ViewChild(CipherFormComponent) + cipherFormComponent: CipherFormComponent | null = null; + + action: CipherFormMode | "view" | null = null; + cipherId: string | null = null; + favorites = false; + type: CipherType | null = null; + folderId: string | null | undefined = null; + collectionId: string | null = null; + organizationId: OrganizationId | null = null; + addType: CipherType | undefined = undefined; + addOrganizationId: string | null = null; + addCollectionIds: string[] | null = null; + showingModal = false; + deleted = false; + activeFilter: VaultFilter = new VaultFilter(); + activeUserId: UserId | null = null; + cipherRepromptId: string | null = null; + readonly cipher = signal(null); + collections: CollectionView[] | null = null; + config: CipherFormConfig | null = null; + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + showPremiumCallout$: Observable = this.userId$.pipe( + switchMap((userId) => + combineLatest([ + this.routedVaultFilterBridgeService.activeFilter$, + this.cipherArchiveService.showSubscriptionEndedMessaging$(userId), + ]).pipe(map(([activeFilter, showMessaging]) => activeFilter.isArchived && showMessaging)), + ), + ); + + /** Tracks the disabled status of the edit cipher form */ + protected formDisabled: boolean = false; + + readonly userHasPremium = toSignal( + this.accountService.activeAccount$.pipe( + filter((account): account is Account => !!account), + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ), + { initialValue: false }, + ); + readonly archiveFlagEnabled = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$, { + initialValue: false, + }); + protected itemTypesIcon = ItemTypes; + + private organizations$: Observable = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + filterOutNullish(), + switchMap((id) => this.organizationService.organizations$(id)), + ); + + protected readonly submitButtonText = computed(() => { + return this.cipher()?.isArchived && + !this.userHasPremium() && + this.cipherArchiveService.hasArchiveFlagEnabled$ + ? this.i18nService.t("unArchiveAndSave") + : this.i18nService.t("save"); + }); + + protected hasArchivedCiphers$ = this.userId$.pipe( + switchMap((userId) => + this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)), + ), + ); + + private componentIsDestroyed$ = new Subject(); + private allOrganizations: Organization[] = []; + private allCollections: CollectionView[] = []; + private filteredCollections: CollectionView[] = []; + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private broadcasterService: BroadcasterService, + private changeDetectorRef: ChangeDetectorRef, + private ngZone: NgZone, + private syncService: SyncService, + private messagingService: MessagingService, + private platformUtilsService: PlatformUtilsService, + private eventCollectionService: EventCollectionService, + private totpService: TotpService, + private passwordRepromptService: PasswordRepromptService, + private searchBarService: SearchBarService, + private dialogService: DialogService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private accountService: AccountService, + private cipherService: CipherService, + private formConfigService: CipherFormConfigService, + private premiumUpgradePromptService: PremiumUpgradePromptService, + private collectionService: CollectionService, + private organizationService: OrganizationService, + private folderService: FolderService, + private authRequestService: AuthRequestServiceAbstraction, + private cipherArchiveService: CipherArchiveService, + private policyService: PolicyService, + private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService, + private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, + private routedVaultFilterService: RoutedVaultFilterService, + private vaultFilterService: VaultFilterService, + private vaultItemTransferService: VaultItemsTransferService, + ) {} + + async ngOnInit() { + // Subscribe to filter changes from router params via the bridge service + // Use combineLatest to react to changes in both the filter and archive flag + combineLatest([ + this.routedVaultFilterBridgeService.activeFilter$, + this.routedVaultFilterService.filter$, + this.cipherArchiveService.hasArchiveFlagEnabled$, + ]) + .pipe( + switchMap(([vaultFilter, routedFilter, archiveEnabled]) => + from(this.applyVaultFilter(vaultFilter, routedFilter, archiveEnabled)), + ), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe(); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone + .run(async () => { + let detectChanges = true; + try { + switch (message.command) { + case "newLogin": + await this.addCipher(CipherType.Login).catch(() => {}); + break; + case "newCard": + await this.addCipher(CipherType.Card).catch(() => {}); + break; + case "newIdentity": + await this.addCipher(CipherType.Identity).catch(() => {}); + break; + case "newSecureNote": + await this.addCipher(CipherType.SecureNote).catch(() => {}); + break; + case "newSshKey": + await this.addCipher(CipherType.SshKey).catch(() => {}); + break; + case "focusSearch": + (document.querySelector("#search") as HTMLInputElement)?.select(); + detectChanges = false; + break; + case "syncCompleted": + if (this.vaultItemsComponent) { + await this.vaultItemsComponent.refresh().catch(() => {}); + } + if (this.activeUserId) { + void this.vaultItemTransferService.enforceOrganizationDataOwnership( + this.activeUserId, + ); + } + break; + case "modalShown": + this.showingModal = true; + break; + case "modalClosed": + this.showingModal = false; + break; + case "copyUsername": { + if (this.cipher()?.login?.username) { + this.copyValue( + this.cipher(), + this.cipher()?.login?.username, + "username", + "Username", + ); + } + break; + } + case "copyPassword": { + if (this.cipher()?.login?.password && this.cipher().viewPassword) { + this.copyValue( + this.cipher(), + this.cipher().login.password, + "password", + "Password", + ); + await this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher().id) + .catch(() => {}); + } + break; + } + case "copyTotp": { + if ( + this.cipher()?.login?.hasTotp && + (this.cipher().organizationUseTotp || this.userHasPremium()) + ) { + const value = await firstValueFrom( + this.totpService.getCode$(this.cipher().login.totp), + ).catch((): any => null); + if (value) { + this.copyValue(this.cipher(), value.code, "verificationCodeTotp", "TOTP"); + } + } + break; + } + default: + detectChanges = false; + break; + } + } catch { + // Ignore errors + } + if (detectChanges) { + this.changeDetectorRef.detectChanges(); + } + }) + .catch(() => {}); + }); + + if (!this.syncService.syncInProgress) { + await this.load().catch(() => {}); + } + + this.searchBarService.setEnabled(true); + this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); + + const authRequests = await firstValueFrom( + this.authRequestService.getLatestPendingAuthRequest$()!, + ); + if (authRequests != null) { + this.messagingService.send("openLoginApproval", { + notificationId: authRequests.id, + }); + } + + this.activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ).catch((): any => null); + + if (this.activeUserId) { + this.cipherService + .failedToDecryptCiphers$(this.activeUserId) + .pipe( + map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); + } + + this.organizations$.pipe(takeUntil(this.componentIsDestroyed$)).subscribe((orgs) => { + this.allOrganizations = orgs; + }); + + if (!this.activeUserId) { + throw new Error("No user found."); + } + + this.collectionService + .decryptedCollections$(this.activeUserId) + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((collections) => { + this.allCollections = collections; + }); + + this.vaultFilterService.filteredCollections$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((collections) => { + this.filteredCollections = collections; + }); + + void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId); + } + + ngOnDestroy() { + this.searchBarService.setEnabled(false); + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.componentIsDestroyed$.next(true); + this.componentIsDestroyed$.complete(); + } + + async load() { + const params = await firstValueFrom(this.route.queryParams).catch(); + const paramCipherAddType = toCipherType(params.addType); + if (params.cipherId) { + const cipherView = new CipherView(); + cipherView.id = params.cipherId; + if (params.action === "clone") { + await this.cloneCipher(cipherView).catch(() => {}); + } else if (params.action === "edit") { + await this.editCipher(cipherView).catch(() => {}); + } else { + await this.viewCipher(cipherView).catch(() => {}); + } + } else if (params.action === "add" && paramCipherAddType) { + this.addType = paramCipherAddType; + await this.addCipher(this.addType).catch(() => {}); + } + } + + /** + * Handler for Vault level CopyClickDirectives to send the minimizeOnCopy message + */ + onCopy() { + this.messagingService.send("minimizeOnCopy"); + } + + async viewCipher(c: CipherViewLike) { + if (CipherViewLikeUtils.decryptionFailure(c)) { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: [c.id as CipherId], + }); + return; + } + const cipher = await this.cipherService.getFullCipherView(c); + if (await this.shouldReprompt(cipher, "view")) { + return; + } + this.cipherId = cipher.id; + this.cipher.set(cipher); + this.collections = + this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null; + this.action = "view"; + + await this.go().catch(() => {}); + await this.eventCollectionService.collect( + EventType.Cipher_ClientViewed, + cipher.id, + false, + cipher.organizationId, + ); + } + + formStatusChanged(status: "disabled" | "enabled") { + this.formDisabled = status === "disabled"; + } + + async openAttachmentsDialog() { + if (!this.userHasPremium()) { + return; + } + const dialogRef = AttachmentsV2Component.open(this.dialogService, { + cipherId: this.cipherId as CipherId, + canEditCipher: this.cipher().edit, + }); + const result = await firstValueFrom(dialogRef.closed).catch((): any => null); + if ( + result?.action === AttachmentDialogResult.Removed || + result?.action === AttachmentDialogResult.Uploaded + ) { + await this.vaultItemsComponent?.refresh().catch(() => {}); + + if (this.cipherFormComponent == null) { + return; + } + + // The encrypted state of ciphers is updated when an attachment is added, + // but the cache is also cleared. Depending on timing, `cipherService.get` can return the + // old cipher. Retrieve the updated cipher from `cipherViews$`, + // which refreshes after the cached is cleared. + const updatedCipherView = await firstValueFrom( + this.cipherService.cipherViews$(this.activeUserId!).pipe( + filter((c) => !!c), + map((ciphers) => ciphers.find((c) => c.id === this.cipherId)), + ), + ); + + // `find` can return undefined but that shouldn't happen as + // this would mean that the cipher was deleted. + // To make TypeScript happy, exit early if it isn't found. + if (!updatedCipherView) { + return; + } + + this.cipherFormComponent.patchCipher((currentCipher) => { + currentCipher.attachments = updatedCipherView.attachments; + currentCipher.revisionDate = updatedCipherView.revisionDate; + + return currentCipher; + }); + } + } + + async viewCipherMenu(c: CipherViewLike) { + const cipher = await this.cipherService.getFullCipherView(c); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const userCanArchive = await firstValueFrom(this.cipherArchiveService.userCanArchive$(userId)); + const orgOwnershipPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId), + ); + + const menu: RendererMenuItem[] = [ + { + label: this.i18nService.t("view"), + click: () => { + this.functionWithChangeDetection(() => { + this.viewCipher(cipher).catch(() => {}); + }); + }, + }, + ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + } + + if (!cipher.isDeleted) { + menu.push({ + label: this.i18nService.t("edit"), + click: () => { + this.functionWithChangeDetection(() => { + this.editCipher(cipher).catch(() => {}); + }); + }, + }); + + const archivedWithOrgOwnership = cipher.isArchived && orgOwnershipPolicy; + const canCloneArchived = !cipher.isArchived || userCanArchive; + + if (!cipher.organizationId && !archivedWithOrgOwnership && canCloneArchived) { + menu.push({ + label: this.i18nService.t("clone"), + click: () => { + this.functionWithChangeDetection(() => { + this.cloneCipher(cipher).catch(() => {}); + }); + }, + }); + } + + const hasEditableCollections = this.allCollections.some((collection) => !collection.readOnly); + + if (cipher.canAssignToCollections && hasEditableCollections) { + menu.push({ + label: this.i18nService.t("assignToCollections"), + click: () => + this.functionWithChangeDetection(async () => { + await this.shareCipher(cipher); + }), + }); + } + } + + if (this.archiveFlagEnabled() && !cipher.isDeleted && !cipher.isArchived) { + menu.push({ + label: this.i18nService.t("archiveVerb"), + click: async () => { + if (!userCanArchive) { + await this.premiumUpgradePromptService.promptForPremium(); + return; + } + + await this.archiveCipherUtilitiesService.archiveCipher(cipher); + await this.refreshCurrentCipher(); + }, + }); + } + + if (cipher.isArchived && !cipher.isDeleted) { + menu.push({ + label: this.i18nService.t("unArchive"), + click: async () => { + await this.archiveCipherUtilitiesService.unarchiveCipher(cipher); + await this.refreshCurrentCipher(); + }, + }); + } + + switch (cipher.type) { + case CipherType.Login: + if ( + cipher.login.canLaunch || + cipher.login.username != null || + cipher.login.password != null + ) { + menu.push({ type: "separator" }); + } + if (cipher.login.canLaunch) { + menu.push({ + label: this.i18nService.t("launch"), + click: () => this.platformUtilsService.launchUri(cipher.login.launchUri), + }); + } + if (cipher.login.username != null) { + menu.push({ + label: this.i18nService.t("copyUsername"), + click: () => this.copyValue(cipher, cipher.login.username, "username", "Username"), + }); + } + if (cipher.login.password != null && cipher.viewPassword) { + menu.push({ + label: this.i18nService.t("copyPassword"), + click: () => { + this.copyValue(cipher, cipher.login.password, "password", "Password"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedPassword, cipher.id) + .catch(() => {}); + }, + }); + } + if (cipher.login.hasTotp && (cipher.organizationUseTotp || this.userHasPremium())) { + menu.push({ + label: this.i18nService.t("copyVerificationCodeTotp"), + click: async () => { + const value = await firstValueFrom( + this.totpService.getCode$(cipher.login.totp), + ).catch((): any => null); + if (value) { + this.copyValue(cipher, value.code, "verificationCodeTotp", "TOTP"); + } + }, + }); + } + break; + case CipherType.Card: + if (cipher.card.number != null || cipher.card.code != null) { + menu.push({ type: "separator" }); + } + if (cipher.card.number != null) { + menu.push({ + label: this.i18nService.t("copyNumber"), + click: () => this.copyValue(cipher, cipher.card.number, "number", "Card Number"), + }); + } + if (cipher.card.code != null) { + menu.push({ + label: this.i18nService.t("copySecurityCode"), + click: () => { + this.copyValue(cipher, cipher.card.code, "securityCode", "Security Code"); + this.eventCollectionService + .collect(EventType.Cipher_ClientCopiedCardCode, cipher.id) + .catch(() => {}); + }, + }); + } + break; + default: + break; + } + invokeMenu(menu); + } + + async shouldReprompt(cipher: CipherView, action: "edit" | "clone" | "view"): Promise { + return !(await this.canNavigateAway(action, cipher)) || !(await this.passwordReprompt(cipher)); + } + + async buildFormConfig(action: CipherFormMode) { + this.config = await this.formConfigService + .buildConfig(action, this.cipherId as CipherId, this.addType) + .catch((): any => null); + } + + async editCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "edit")) { + return; + } + this.cipherId = cipher.id; + this.cipher.set(cipher); + await this.buildFormConfig("edit"); + if (!cipher.edit && this.config) { + this.config.mode = "partial-edit"; + } + this.action = "edit"; + await this.go().catch(() => {}); + } + + async cloneCipher(cipher: CipherView) { + if (await this.shouldReprompt(cipher, "clone")) { + return; + } + this.cipherId = cipher.id; + this.cipher.set(cipher); + await this.buildFormConfig("clone"); + this.action = "clone"; + await this.go().catch(() => {}); + } + + async shareCipher(cipher: CipherView) { + if (!cipher) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nothingSelected"), + }); + return; + } + + if (!(await this.passwordReprompt(cipher))) { + return; + } + + const availableCollections = this.getAvailableCollections(cipher); + + const dialog = AssignCollectionsDesktopComponent.open(this.dialogService, { + data: { + ciphers: [cipher], + organizationId: cipher.organizationId as OrganizationId, + availableCollections, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionAssignmentResult.Saved) { + const updatedCipher = await firstValueFrom( + // Fetch the updated cipher from the service + this.cipherService.cipherViews$(this.activeUserId as UserId).pipe( + filter((ciphers) => ciphers != null), + map((ciphers) => ciphers!.find((c) => c.id === cipher.id)), + filter((foundCipher) => foundCipher != null), + ), + ); + await this.savedCipher(updatedCipher); + } + } + + async addCipher(type: CipherType) { + if (this.action === "add") { + return; + } + this.addType = type || this.activeFilter.cipherType; + this.cipher.set(new CipherView()); + this.cipherId = null; + await this.buildFormConfig("add"); + this.action = "add"; + this.prefillCipherFromFilter(); + await this.go().catch(() => {}); + + if (type === CipherType.SshKey) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } + } + + async savedCipher(cipher: CipherView) { + this.cipherId = null; + this.action = "view"; + await this.vaultItemsComponent?.refresh().catch(() => {}); + + if (!this.activeUserId) { + throw new Error("No userId provided."); + } + + this.collections = await firstValueFrom( + this.collectionService + .decryptedCollections$(this.activeUserId) + .pipe(getByIds(cipher.collectionIds)), + ); + + this.cipherId = cipher.id; + this.cipher.set(cipher); + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async deleteCipher() { + this.cipherId = null; + this.cipher.set(null); + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async restoreCipher() { + this.cipherId = null; + this.action = null; + await this.go().catch(() => {}); + await this.vaultItemsComponent?.refresh().catch(() => {}); + } + + async cancelCipher(cipher: CipherView) { + this.cipherId = cipher.id; + this.cipher.set(cipher); + this.action = this.cipherId ? "view" : null; + await this.go().catch(() => {}); + } + + async applyVaultFilter( + vaultFilter: VaultFilter, + routedFilter: Parameters[0], + archiveEnabled: boolean, + ) { + this.searchBarService.setPlaceholderText( + this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), + ); + this.activeFilter = vaultFilter; + + const filterFn = createFilterFunction(routedFilter, archiveEnabled); + + await this.vaultItemsComponent?.reload(filterFn, vaultFilter.isDeleted, vaultFilter.isArchived); + } + + private getAvailableCollections(cipher: CipherView): CollectionView[] { + const orgId = cipher.organizationId; + if (!orgId || orgId === "MyVault") { + return []; + } + + const organization = this.allOrganizations.find((o) => o.id === orgId); + return this.allCollections.filter((c) => c.organizationId === organization?.id && !c.readOnly); + } + + private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { + if (vaultFilter.isFavorites) { + return "searchFavorites"; + } + if (vaultFilter.isDeleted) { + return "searchTrash"; + } + if (vaultFilter.cipherType != null) { + return "searchType"; + } + if (vaultFilter.folderId != null && vaultFilter.folderId !== "none") { + return "searchFolder"; + } + if (vaultFilter.collectionId != null) { + return "searchCollection"; + } + if (vaultFilter.organizationId != null) { + return "searchOrganization"; + } + if (vaultFilter.isMyVaultSelected) { + return "searchMyVault"; + } + return "searchVault"; + } + + async addFolder() { + this.messagingService.send("newFolder"); + } + + async editFolder(folderId: string) { + if (!this.activeUserId) { + return; + } + const folderView = await firstValueFrom( + this.folderService.getDecrypted$(folderId, this.activeUserId), + ); + + if (!folderView) { + return; + } + } + + /** Refresh the current cipher object */ + protected async refreshCurrentCipher() { + if (!this.cipher()) { + return; + } + + this.cipher.set( + await firstValueFrom( + this.cipherService.cipherViews$(this.activeUserId!).pipe( + filter((c) => !!c), + map((ciphers) => ciphers.find((c) => c.id === this.cipherId) ?? null), + ), + ), + ); + } + + private dirtyInput(): boolean { + return ( + (this.action === "add" || this.action === "edit" || this.action === "clone") && + document.querySelectorAll("vault-cipher-form .ng-dirty").length > 0 + ); + } + + private async wantsToSaveChanges(): Promise { + const confirmed = await this.dialogService + .openSimpleDialog({ + title: { key: "unsavedChangesTitle" }, + content: { key: "unsavedChangesConfirmation" }, + type: "warning", + }) + .catch(() => false); + return !confirmed; + } + + private async go(queryParams: any = null) { + if (queryParams == null) { + queryParams = { + action: this.action, + cipherId: this.cipherId, + }; + } + this.router + .navigate([], { + relativeTo: this.route, + queryParams: queryParams, + queryParamsHandling: "merge", + replaceUrl: true, + }) + .catch(() => {}); + } + + private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) { + this.functionWithChangeDetection(() => { + (async () => { + if ( + cipher.reprompt !== CipherRepromptType.None && + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.passwordReprompt(cipher)) + ) { + return; + } + this.platformUtilsService.copyToClipboard(value); + this.toastService.showToast({ + variant: "info", + title: undefined, + message: this.i18nService.t("valueCopied", this.i18nService.t(labelI18nKey)), + }); + this.messagingService.send("minimizeOnCopy"); + })().catch(() => {}); + }); + } + + private functionWithChangeDetection(func: () => void) { + this.ngZone.run(() => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } + + private prefillCipherFromFilter() { + if (this.activeFilter.collectionId != null) { + const collections = this.filteredCollections?.filter( + (c) => c.id === this.activeFilter.collectionId, + ); + if (collections?.length > 0) { + this.addOrganizationId = collections[0].organizationId; + this.addCollectionIds = [this.activeFilter.collectionId]; + } + } else if (this.activeFilter.organizationId && this.activeFilter.organizationId !== "MyVault") { + this.addOrganizationId = this.activeFilter.organizationId; + } else { + // clear out organizationId when the user switches to a personal vault filter + this.addOrganizationId = null; + } + if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) { + this.folderId = this.activeFilter.folderId; + } + + if (this.config == null) { + return; + } + + this.config.initialValues = { + ...this.config.initialValues, + folderId: this.folderId, + organizationId: this.addOrganizationId as OrganizationId, + collectionIds: this.addCollectionIds as CollectionId[], + }; + } + + private async canNavigateAway(action: string, cipher?: CipherView) { + if (this.action === action && (!cipher || this.cipherId === cipher.id)) { + return false; + } else if (this.dirtyInput() && (await this.wantsToSaveChanges())) { + return false; + } + return true; + } + + private async passwordReprompt(cipher: CipherView) { + if (cipher.reprompt === CipherRepromptType.None) { + this.cipherRepromptId = null; + return true; + } + if (this.cipherRepromptId === cipher.id) { + return true; + } + const repromptResult = await this.passwordRepromptService.showPasswordPrompt(); + if (repromptResult) { + this.cipherRepromptId = cipher.id; + } + return repromptResult; + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-wrapper.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-wrapper.component.ts new file mode 100644 index 00000000000..7a5de8cb9c8 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-wrapper.component.ts @@ -0,0 +1,28 @@ +import { CommonModule } from "@angular/common"; +import { Component, computed, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { VaultComponent as VaultOrigComponent } from "./vault-orig.component"; +import { VaultComponent } from "./vault.component"; + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + selector: "app-vault-wrapper", + template: '', + imports: [CommonModule], +}) +export class VaultWrapperComponent { + private configService: ConfigService = inject(ConfigService); + + protected readonly useMilestone3 = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone3), + ); + + protected readonly componentToRender = computed(() => + this.useMilestone3() ? VaultComponent : VaultOrigComponent, + ); +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html index 5d8c3491710..4a0b37f2d80 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.html +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html @@ -1,38 +1,53 @@ -
- - - @if (!!action) { +
+
+
+ + + +
+ + +
+ @if (!!action()) {
-
- @if (action === "view") { - + @if (action() === "view") { + } - @if (action === "add" || action === "edit" || action === "clone") { + @if (action() === "add" || action() === "edit" || action() === "clone") {
{{ "attachments" | i18n }} - +
@@ -60,9 +75,23 @@
+
} - @if (!["add", "edit", "view", "clone"].includes(action)) { + @if (!["add", "edit", "view", "clone"].includes(action())) { }
diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts b/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts new file mode 100644 index 00000000000..4db94d5d717 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts @@ -0,0 +1,172 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { ButtonModule, DialogService, ToastService } from "@bitwarden/components"; +import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitwarden/vault"; + +import { ItemFooterComponent } from "./item-footer.component"; + +describe("ItemFooterComponent", () => { + let component: ItemFooterComponent; + let fixture: ComponentFixture; + let accountService: FakeAccountService; + + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(async () => { + accountService = mockAccountServiceWith(mockUserId); + + const cipherArchiveService = { + userCanArchive$: jest.fn().mockReturnValue(of(false)), + hasArchiveFlagEnabled$: of(false), + }; + + await TestBed.configureTestingModule({ + imports: [ItemFooterComponent, ButtonModule, JslibModule], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: CipherService, useValue: mock() }, + { provide: DialogService, useValue: mock() }, + { provide: PasswordRepromptService, useValue: mock() }, + { provide: CipherAuthorizationService, useValue: mock() }, + { provide: ToastService, useValue: mock() }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: LogService, useValue: mock() }, + { provide: CipherArchiveService, useValue: cipherArchiveService }, + { provide: ArchiveCipherUtilitiesService, useValue: mock() }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ItemFooterComponent); + component = fixture.componentInstance; + }); + + const createCipherView = (overrides: Partial = {}): CipherView => { + const cipher = new CipherView(); + cipher.id = "test-cipher-id"; + cipher.permissions = { + delete: false, + restore: false, + manage: false, + edit: false, + view: false, + viewPassword: false, + }; + return Object.assign(cipher, overrides); + }; + + describe("delete button visibility", () => { + it("shows the delete button when cipher.permissions.delete is true and action is 'view'", async () => { + const cipher = createCipherView({ + permissions: { + delete: true, + restore: false, + manage: false, + edit: false, + view: false, + viewPassword: false, + }, + }); + + component.cipher = cipher; + component.action = "view"; + + await component.ngOnInit(); + fixture.detectChanges(); + + const deleteButton = fixture.debugElement.query( + By.css('[data-test-id="footer-delete-button"]'), + ); + + expect(deleteButton).toBeTruthy(); + }); + + it("shows the delete button when cipher.permissions.delete is true and action is 'edit'", async () => { + const cipher = createCipherView({ + permissions: { + delete: true, + restore: false, + manage: false, + edit: false, + view: false, + viewPassword: false, + }, + }); + + component.cipher = cipher; + component.action = "edit"; + + await component.ngOnInit(); + fixture.detectChanges(); + + const deleteButton = fixture.debugElement.query( + By.css('[data-test-id="footer-delete-button"]'), + ); + + expect(deleteButton).toBeTruthy(); + }); + + it("does not show the delete button when cipher.permissions.delete is false", async () => { + const cipher = createCipherView({ + permissions: { + delete: false, + restore: false, + manage: false, + edit: false, + view: false, + viewPassword: false, + }, + }); + + component.cipher = cipher; + component.action = "view"; + + await component.ngOnInit(); + fixture.detectChanges(); + + const deleteButton = fixture.debugElement.query( + By.css('[data-test-id="footer-delete-button"]'), + ); + + expect(deleteButton).toBeFalsy(); + }); + + it("does not show the delete button when action is not 'view' or 'edit'", async () => { + const cipher = createCipherView({ + permissions: { + delete: true, + restore: false, + manage: false, + edit: false, + view: false, + viewPassword: false, + }, + }); + + component.cipher = cipher; + component.action = "add"; + + await component.ngOnInit(); + fixture.detectChanges(); + + const deleteButton = fixture.debugElement.query( + By.css('[data-test-id="footer-delete-button"]'), + ); + + expect(deleteButton).toBeFalsy(); + }); + }); +}); diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts index 133a9777fab..891baf716c1 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.ts +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -128,11 +128,7 @@ export class ItemFooterComponent implements OnInit, OnChanges { } protected get hasFooterAction() { - return ( - this.showArchiveButton || - this.showUnarchiveButton || - (this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view")) - ); + return this.showArchiveButton || this.showUnarchiveButton || this.canDelete; } protected get showCloneOption() { @@ -145,6 +141,10 @@ export class ItemFooterComponent implements OnInit, OnChanges { ); } + protected get canDelete() { + return this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view"); + } + cancel() { this.onCancel.emit(this.cipher); } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts index 95ffd3f0212..5b9c32291dd 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component, viewChild } from "@angular/core"; import { combineLatest, firstValueFrom, map, switchMap } from "rxjs"; diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 458ddd666b8..a63d4528b3e 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { ChangeDetectorRef, @@ -10,6 +12,7 @@ import { ViewChild, ViewContainerRef, } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, @@ -209,6 +212,9 @@ export class VaultV2Component collections: CollectionView[] | null = null; config: CipherFormConfig | null = null; readonly userHasPremium = signal(false); + readonly archiveFlagEnabled = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$, { + initialValue: false, + }); protected itemTypesIcon = ItemTypes; /** Tracks the disabled status of the edit cipher form */ @@ -618,7 +624,7 @@ export class VaultV2Component } } - if (userCanArchive && !cipher.isDeleted && !cipher.isArchived) { + if (this.archiveFlagEnabled() && !cipher.isDeleted && !cipher.isArchived) { menu.push({ label: this.i18nService.t("archiveVerb"), click: async () => { @@ -1052,7 +1058,7 @@ export class VaultV2Component ...this.config.initialValues, organizationId: this.addOrganizationId as OrganizationId, folderId: this.folderId, - collectionIds: this.addCollectionIds as CollectionId[], + collectionIds: this.addCollectionIds ? (this.addCollectionIds as CollectionId[]) : [], }; } diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 27036e16240..96e973de889 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -9,18 +9,23 @@ COPY package*.json ./ COPY . . RUN npm ci -# Remove commercial packages if LICENSE_TYPE is not 'commercial' -ARG LICENSE_TYPE=oss -RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \ - rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \ - fi - # Override SDK if custom artifacts are present RUN if [ -d "sdk-internal" ]; then \ echo "Overriding SDK with custom artifacts from sdk-internal" ; \ npm link ./sdk-internal ; \ fi +RUN if [ -d "commercial-sdk-internal" ]; then \ + echo "Overriding Commercial SDK with custom artifacts from commercial-sdk-internal" ; \ + npm link ./commercial-sdk-internal ; \ + fi + +# Remove commercial packages if LICENSE_TYPE is not 'commercial' +ARG LICENSE_TYPE=oss +RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \ + rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \ + fi + WORKDIR /source/apps/web ARG NPM_COMMAND=dist:bit:selfhost RUN npm run ${NPM_COMMAND} diff --git a/apps/web/package.json b/apps/web/package.json index ad778b03778..844ac1f12b5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2026.2.0", + "version": "2026.2.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index ba315dee7fb..dd1c393bc13 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Directive, OnDestroy } from "@angular/core"; +import { Directive, OnDestroy, signal } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, filter, map, Observable, Subject, switchMap, takeUntil } from "rxjs"; @@ -22,9 +22,9 @@ import { EventExportService } from "../../tools/event-export"; @Directive() export abstract class BaseEventsComponent implements OnDestroy { - loading = true; - loaded = false; - events: EventView[]; + readonly loading = signal(true); + readonly loaded = signal(false); + readonly events = signal([]); dirtyDates = true; continuationToken: string; canUseSM = false; @@ -115,7 +115,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); const dates = this.parseDates(); if (dates == null) { @@ -131,7 +131,7 @@ export abstract class BaseEventsComponent implements OnDestroy { } promise = null; - this.loading = false; + this.loading.set(false); }; loadEvents = async (clearExisting: boolean) => { @@ -140,7 +140,7 @@ export abstract class BaseEventsComponent implements OnDestroy { return; } - this.loading = true; + this.loading.set(true); let events: EventView[] = []; let promise: Promise; promise = this.loadAndParseEvents( @@ -153,14 +153,16 @@ export abstract class BaseEventsComponent implements OnDestroy { this.continuationToken = result.continuationToken; events = result.events; - if (!clearExisting && this.events != null && this.events.length > 0) { - this.events = this.events.concat(events); + if (!clearExisting && this.events() != null && this.events().length > 0) { + this.events.update((current) => { + return [...current, ...events]; + }); } else { - this.events = events; + this.events.set(events); } this.dirtyDates = false; - this.loading = false; + this.loading.set(false); promise = null; }; @@ -227,7 +229,7 @@ export abstract class BaseEventsComponent implements OnDestroy { private async export(start: string, end: string) { let continuationToken = this.continuationToken; - let events = [].concat(this.events); + let events = [].concat(this.events()); while (continuationToken != null) { const result = await this.loadAndParseEvents(start, end, continuationToken); diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 073d73f6a50..1d9178e6fed 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Params, Router } from "@angular/router"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -472,7 +472,7 @@ export class VaultComponent implements OnInit, OnDestroy { collections, filter.collectionId, ); - searchableCollectionNodes = selectedCollection.children ?? []; + searchableCollectionNodes = selectedCollection?.children ?? []; } let collectionsToReturn: CollectionAdminView[] = []; @@ -588,7 +588,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -812,7 +812,7 @@ export class VaultComponent implements OnInit, OnDestroy { async editCipherAttachments(cipher: CipherView) { if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -869,7 +869,7 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - this.go({ cipherId: null, itemId: null }); + this.go({ cipherId: null, itemId: null }, this.configureRouterFocusToCipher(cipher.id)); return; } @@ -893,7 +893,10 @@ export class VaultComponent implements OnInit, OnDestroy { !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -943,7 +946,10 @@ export class VaultComponent implements OnInit, OnDestroy { } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } async cloneCipher(cipher: CipherView) { @@ -962,10 +968,10 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCipher(cipher, true); } - restore = async (c: CipherViewLike): Promise => { + restore = async (c: CipherViewLike): Promise => { const organization = await firstValueFrom(this.organization$); if (!CipherViewLikeUtils.isDeleted(c)) { - return false; + return; } if ( @@ -974,11 +980,11 @@ export class VaultComponent implements OnInit, OnDestroy { !organization.allowAdminAccessToAllCollectionItems ) { this.showMissingPermissionsError(); - return false; + return; } if (!(await this.repromptCipher([c]))) { - return false; + return; } // Allow restore of an Unassigned Item @@ -996,10 +1002,10 @@ export class VaultComponent implements OnInit, OnDestroy { message: this.i18nService.t("restoredItem"), }); this.refresh(); - return true; + return; } catch (e) { this.logService.error(e); - return false; + return; } }; @@ -1422,7 +1428,25 @@ export class VaultComponent implements OnInit, OnDestroy { } } - private go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { type: this.activeFilter.cipherType, @@ -1436,6 +1460,7 @@ export class VaultComponent implements OnInit, OnDestroy { queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 83665a4b99e..3e76c8c879b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,14 +1,16 @@ @let usePlaceHolderEvents = !organization?.useEvents; + - - {{ "upgrade" | i18n }} - + @if (usePlaceHolderEvents) { + + {{ "upgrade" | i18n }} + + }
@@ -61,79 +63,87 @@
- - {{ "upgradeEventLogMessage" | i18n }} - - - - {{ "loading" | i18n }} - - - @let displayedEvents = organization?.useEvents ? events : placeholderEvents; +@if (loaded() && usePlaceHolderEvents) { + + {{ "upgradeEventLogMessage" | i18n }} + +} -

{{ "noEventsInList" | i18n }}

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

{{ "noEventsInList" | i18n }}

+ } -

- {{ "upgradeEventLogTitleMessage" | i18n }} -

-

- {{ "upgradeForFullEventsMessage" | i18n }} -

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

+ {{ "upgradeEventLogTitleMessage" | i18n }} +

+

+ {{ "upgradeForFullEventsMessage" | i18n }} +

+ + +
-
- + +} diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 62f6539cc16..01d6515c486 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, firstValueFrom, lastValueFrom, map, of, switchMap, takeUntil, tap } from "rxjs"; @@ -44,11 +44,11 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM [EventSystemUser.DomainVerification]: "domainVerification", [EventSystemUser.PublicApi]: "publicApi", + [EventSystemUser.BitwardenPortal]: "system", }; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: "events.component.html", imports: [SharedModule, HeaderModule], }) @@ -167,7 +167,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe } } await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index bd3dd7fbb0b..8fd00e507ce 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -81,7 +81,7 @@ export class OrganizationUserResetPasswordService implements UserKeyRotationKeyR if ( !trustedPublicKeys.some( - (key) => Utils.fromBufferToHex(key) === Utils.fromBufferToHex(publicKey), + (key) => Utils.fromArrayToHex(key) === Utils.fromArrayToHex(publicKey), ) ) { throw new Error("Untrusted public key"); diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index e8e48f41716..f659fad7b51 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -6,8 +6,10 @@ import { Constructor } from "type-fest"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { VNextSavePolicyRequest } from "@bitwarden/common/admin-console/models/request/v-next-save-policy.request"; import { PolicyStatusResponse } from "@bitwarden/common/admin-console/models/response/policy-status.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { OrgKey } from "@bitwarden/common/types/key"; import { DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import type { PolicyEditDialogData, PolicyEditDialogResult } from "./policy-edit-dialog.component"; @@ -56,7 +58,7 @@ export abstract class BasePolicyEditDefinition { * If true, the {@link description} will be reused in the policy edit modal. Set this to false if you * have more complex requirements that you will implement in your template instead. **/ - showDescription: boolean = false; + showDescription: boolean = true; /** * A method that determines whether to display this policy in the Admin Console Policies page. @@ -103,6 +105,19 @@ export abstract class BasePolicyEditComponent implements OnInit { } } + async buildVNextRequest(orgKey: OrgKey): Promise { + if (!this.policy) { + throw new Error("Policy was not found"); + } + + const request: VNextSavePolicyRequest = { + policy: await this.buildRequest(), + metadata: null, + }; + + return request; + } + buildRequest() { if (!this.policy) { throw new Error("Policy was not found"); diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 64fb4794037..902c7e79d55 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -19,14 +19,12 @@ @if (p.display$(organization, configService) | async) { -
- - @if (policiesEnabledMap.get(p.type)) { - {{ "on" | i18n }} - } -
+ + @if (policiesEnabledMap.get(p.type)) { + {{ "on" | i18n }} + } {{ p.description | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 0dd0b67c189..d13a2097628 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, DestroyRef, OnDestroy } from "@angular/core"; +import { ChangeDetectionStrategy, Component, DestroyRef } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, Observable, of, switchMap, first, map, shareReplay } from "rxjs"; @@ -14,7 +14,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { getById } from "@bitwarden/common/platform/misc"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { DialogRef, DialogService } from "@bitwarden/components"; +import { DialogService } from "@bitwarden/components"; import { safeProvider } from "@bitwarden/ui-common"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -37,8 +37,7 @@ import { POLICY_EDIT_REGISTER } from "./policy-register-token"; ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PoliciesComponent implements OnDestroy { - private myDialogRef?: DialogRef; +export class PoliciesComponent { private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); protected organizationId$: Observable = this.route.params.pipe( @@ -99,10 +98,6 @@ export class PoliciesComponent implements OnDestroy { this.handleLaunchEvent(); } - ngOnDestroy() { - this.myDialogRef?.close(); - } - // Handle policies component launch from Event message private handleLaunchEvent() { combineLatest([ @@ -136,7 +131,7 @@ export class PoliciesComponent implements OnDestroy { edit(policy: BasePolicyEditDefinition, organizationId: OrganizationId) { const dialogComponent: PolicyDialogComponent = policy.editDialogComponent ?? PolicyEditDialogComponent; - this.myDialogRef = dialogComponent.open(this.dialogService, { + dialogComponent.open(this.dialogService, { data: { policy: policy, organizationId: organizationId, diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.html index 2267bd5888e..f979c143a3a 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.html @@ -13,24 +13,29 @@ {{ "enforceOnLoginDesc" | i18n }} - - {{ "minComplexityScore" | i18n }} - - - - - - - {{ "minLength" | i18n }} - - +
+ + {{ "minComplexityScore" | i18n }} + + + + + + {{ "minLength" | i18n }} + + +
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts index e4a07b7440d..bae94940b05 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts @@ -3,7 +3,7 @@ import { lastValueFrom, map, Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { VNextSavePolicyRequest } from "@bitwarden/common/admin-console/models/request/v-next-save-policy.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -15,12 +15,9 @@ import { EncString } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../../shared"; import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; -export interface VNextPolicyRequest { - policy: PolicyRequest; - metadata: { - defaultUserCollectionName: string; - }; -} +type VNextSaveOrganizationDataOwnershipPolicyRequest = VNextSavePolicyRequest<{ + defaultUserCollectionName: string; +}>; export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { name = "organizationDataOwnership"; @@ -69,14 +66,16 @@ export class OrganizationDataOwnershipPolicyComponent return true; } - async buildVNextRequest(orgKey: OrgKey): Promise { + async buildVNextRequest( + orgKey: OrgKey, + ): Promise { if (!this.policy) { throw new Error("Policy was not found"); } const defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); - const request: VNextPolicyRequest = { + const request: VNextSaveOrganizationDataOwnershipPolicyRequest = { policy: { enabled: this.enabled.value ?? false, data: this.buildRequestData(), diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.html index 372ad3609c0..1300acee471 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.html @@ -4,50 +4,56 @@ {{ "turnOn" | i18n }} - - {{ "passwordTypePolicyOverride" | i18n }} - - - - +
+ + {{ "overridePasswordTypePolicy" | i18n }} + + + + +

{{ "password" | i18n }}

- - {{ "minLength" | i18n }} - - - - {{ "minNumbers" | i18n }} - - - - {{ "minSpecial" | i18n }} - - +
+ + {{ "minLength" | i18n }} + + +
+
+ + {{ "minNumbers" | i18n }} + + + + {{ "minSpecial" | i18n }} + + +
{{ "uppercaseLabel" | i18n }} @@ -73,16 +79,18 @@

{{ "passphrase" | i18n }}

- - {{ "minimumNumberOfWords" | i18n }} - - +
+ + {{ "minimumNumberOfWords" | i18n }} + + +
{{ "capitalize" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.spec.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.spec.ts index 21ab7fc71ba..da0e9ede217 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.spec.ts @@ -6,6 +6,7 @@ import { mock } from "jest-mock-extended"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyStatusResponse } from "@bitwarden/common/admin-console/models/response/policy-status.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrgKey } from "@bitwarden/common/types/key"; import { RemoveUnlockWithPinPolicy, @@ -96,4 +97,27 @@ describe("RemoveUnlockWithPinPolicyComponent", () => { expect(bitLabelElement).not.toBeNull(); expect(bitLabelElement.nativeElement.textContent.trim()).toBe("Turn on"); }); + + it("buildVNextRequest should delegate to buildRequest and wrap with null metadata", async () => { + component.policy = new RemoveUnlockWithPinPolicy(); + component.policyResponse = new PolicyStatusResponse({ + organizationId: "org1", + type: PolicyType.RemoveUnlockWithPin, + enabled: true, + }); + component.ngOnInit(); + + const buildRequestSpy = jest.spyOn(component, "buildRequest"); + + const result = await component.buildVNextRequest(mock()); + + expect(buildRequestSpy).toHaveBeenCalled(); + expect(result).toEqual({ + policy: { + enabled: true, + data: null, + }, + metadata: null, + }); + }); }); diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts index e1b2f14d457..334d964f0bf 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts @@ -12,7 +12,7 @@ import { Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; +import { VNextSavePolicyRequest } from "@bitwarden/common/admin-console/models/request/v-next-save-policy.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -24,12 +24,9 @@ import { SharedModule } from "../../../../shared"; import { BasePolicyEditDefinition, BasePolicyEditComponent } from "../base-policy-edit.component"; import { OrganizationDataOwnershipPolicyDialogComponent } from "../policy-edit-dialogs"; -export interface VNextPolicyRequest { - policy: PolicyRequest; - metadata: { - defaultUserCollectionName: string; - }; -} +type VNextSaveOrganizationDataOwnershipPolicyRequest = VNextSavePolicyRequest<{ + defaultUserCollectionName: string; +}>; export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { name = "centralizeDataOwnership"; @@ -67,14 +64,16 @@ export class vNextOrganizationDataOwnershipPolicyComponent protected steps = [this.policyForm, this.warningContent]; - async buildVNextRequest(orgKey: OrgKey): Promise { + async buildVNextRequest( + orgKey: OrgKey, + ): Promise { if (!this.policy) { throw new Error("Policy was not found"); } const defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); - const request: VNextPolicyRequest = { + const request: VNextSaveOrganizationDataOwnershipPolicyRequest = { policy: { enabled: this.enabled.value ?? false, data: this.buildRequestData(), diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html index 6981802e8e9..6573801ad25 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html @@ -1,13 +1,5 @@ -
- - - - {{ policy.name | i18n }} - @if (isPolicyEnabled) { - {{ "on" | i18n }} - } - - + +
Promise; + buildVNextRequest: (orgKey: OrgKey) => Promise; } { return "buildVNextRequest" in component && typeof component.buildVNextRequest === "function"; } @@ -145,11 +141,7 @@ export class PolicyEditDialogComponent implements AfterViewInit { } try { - if (this.hasVNextRequest(this.policyComponent)) { - await this.handleVNextSubmission(this.policyComponent); - } else { - await this.handleStandardSubmission(); - } + await this.handleVNextSubmission(this.policyComponent); this.toastService.showToast({ variant: "success", @@ -164,20 +156,7 @@ export class PolicyEditDialogComponent implements AfterViewInit { } }; - private async handleStandardSubmission(): Promise { - if (!this.policyComponent) { - throw new Error("PolicyComponent not initialized."); - } - - const request = await this.policyComponent.buildRequest(); - await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request); - } - - private async handleVNextSubmission( - policyComponent: BasePolicyEditComponent & { - buildVNextRequest: (orgKey: OrgKey) => Promise; - }, - ): Promise { + private async handleVNextSubmission(policyComponent: BasePolicyEditComponent): Promise { const orgKey = await firstValueFrom( this.accountService.activeAccount$.pipe( getUserId, @@ -200,9 +179,6 @@ export class PolicyEditDialogComponent implements AfterViewInit { ); } static open = (dialogService: DialogService, config: DialogConfig) => { - return dialogService.openDrawer( - PolicyEditDialogComponent, - config, - ); + return dialogService.open(PolicyEditDialogComponent, config); }; } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html index fd0867a5cd5..4d1db65034d 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.html @@ -1,5 +1,5 @@ - - + + @let title = (multiStepSubmit | async)[currentStep()]?.titleContent(); @if (title) { @@ -40,16 +40,13 @@ @if (showBadge) { {{ "availableNow" | i18n }} } - + {{ (showBadge ? "autoConfirm" : "editPolicy") | i18n }} @if (!showBadge) { {{ policy.name | i18n }} } - @if (isPolicyEnabled) { - {{ "on" | i18n }} - }
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts index b5a92671054..f0146225b8d 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/auto-confirm-edit-policy-dialog.component.ts @@ -287,9 +287,6 @@ export class AutoConfirmPolicyDialogComponent dialogService: DialogService, config: DialogConfig, ) => { - return dialogService.openDrawer( - AutoConfirmPolicyDialogComponent, - config, - ); + return dialogService.open(AutoConfirmPolicyDialogComponent, config); }; } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html index 7b89f4d0acc..73691e94199 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.html @@ -1,5 +1,5 @@ - - + + @let title = multiStepSubmit()[currentStep()]?.titleContent(); @if (title) { @@ -35,12 +35,7 @@ - - {{ policy.name | i18n }} - @if (isPolicyEnabled) { - {{ "on" | i18n }} - } - + {{ policy.name | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts index c62be85287f..7869eab0063 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/organization-data-ownership-edit-policy-dialog.component.ts @@ -216,7 +216,7 @@ export class OrganizationDataOwnershipPolicyDialogComponent }; static open = (dialogService: DialogService, config: DialogConfig) => { - return dialogService.openDrawer( + return dialogService.open( OrganizationDataOwnershipPolicyDialogComponent, config, ); diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts index 46599d7da46..d96e2cbb6c0 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { NgModule } from "@angular/core"; import { ReportsSharedModule } from "../../../dirt/reports"; @@ -8,7 +9,13 @@ import { OrganizationReportingRoutingModule } from "./organization-reporting-rou import { ReportsHomeComponent } from "./reports-home.component"; @NgModule({ - imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule], + imports: [ + SharedModule, + OverlayModule, + ReportsSharedModule, + OrganizationReportingRoutingModule, + HeaderModule, + ], declarations: [ReportsHomeComponent], }) export class OrganizationReportingModule {} diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html index 59eac5b6300..9a931f66af9 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.html @@ -8,9 +8,26 @@ - +@if (!(homepage$ | async)) { + + +} + + + + diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 6043bfd3193..503a4f88050 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -1,6 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + OnInit, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs"; @@ -21,16 +33,30 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/r templateUrl: "reports-home.component.html", standalone: false, }) -export class ReportsHomeComponent implements OnInit { +export class ReportsHomeComponent implements OnInit, AfterViewInit, OnDestroy { reports$: Observable; homepage$: Observable; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); + + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + constructor( private route: ActivatedRoute, private organizationService: OrganizationService, private accountService: AccountService, private router: Router, - ) {} + ) { + this.router.events + .pipe( + takeUntilDestroyed(), + filter((event) => event instanceof NavigationEnd), + ) + .subscribe(() => this.updateOverlay()); + } async ngOnInit() { this.homepage$ = this.router.events.pipe( @@ -51,6 +77,46 @@ export class ReportsHomeComponent implements OnInit { ); } + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.isReportsHomepageRouteUrl(this.router.url)) { + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), + }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } + } + private buildReports(productType: ProductTierType): ReportEntry[] { const reportRequiresUpgrade = productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled; diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index 27a6226f964..13467e222d2 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -4,9 +4,9 @@ import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/pre import { ItemModule } from "@bitwarden/components"; import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { AccountComponent } from "./account.component"; import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module"; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts index 3e23eff13a9..f5c6939c284 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts @@ -25,7 +25,7 @@ const render: Story["render"] = (args) => ({ ...args, }, template: ` - + Access selector

- - {{ "loading" | i18n }} +

diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index cc08b840c30..63bfbf3ef0b 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -2,12 +2,11 @@

- - {{ "loading" | i18n }} +

diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html index 08b4851899d..c9d80a2c947 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html @@ -1,7 +1,7 @@
- + {{ "loading" | i18n }}

{{ "pickAnAvatarColor" | i18n }}

@@ -30,10 +30,11 @@ class="tw-relative tw-flex tw-size-16 tw-cursor-pointer tw-place-content-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > - + class="!tw-text-muted tw-m-auto tw-text-3xl" + > { let component: ChangeEmailComponent; let fixture: ComponentFixture; - let apiService: MockProxy; + let changeEmailService: MockProxy; let twoFactorService: MockProxy; let accountService: FakeAccountService; - let keyService: MockProxy; - let kdfConfigService: MockProxy; beforeEach(async () => { - apiService = mock(); + changeEmailService = mock(); twoFactorService = mock(); - keyService = mock(); - kdfConfigService = mock(); accountService = mockAccountServiceWith("UserId" as UserId); await TestBed.configureTestingModule({ imports: [ReactiveFormsModule, SharedModule, ChangeEmailComponent], providers: [ { provide: AccountService, useValue: accountService }, - { provide: ApiService, useValue: apiService }, { provide: TwoFactorService, useValue: twoFactorService }, { provide: I18nService, useValue: { t: (key: string) => key } }, - { provide: KeyService, useValue: keyService }, { provide: MessagingService, useValue: mock() }, - { provide: KdfConfigService, useValue: kdfConfigService }, - { provide: ToastService, useValue: mock() }, { provide: FormBuilder, useClass: FormBuilder }, + { provide: ToastService, useValue: mock() }, + { provide: ChangeEmailService, useValue: changeEmailService }, ], }).compileComponents(); @@ -87,17 +80,11 @@ describe("ChangeEmailComponent", () => { describe("submit", () => { beforeEach(() => { + component.userId = "UserId" as UserId; component.formGroup.controls.step1.setValue({ masterPassword: "password", newEmail: "test@example.com", }); - - keyService.getOrDeriveMasterKey - .calledWith("password", "UserId" as UserId) - .mockResolvedValue("getOrDeriveMasterKey" as any); - keyService.hashMasterKey - .calledWith("password", "getOrDeriveMasterKey" as any) - .mockResolvedValue("existingHash"); }); it("throws if userId is null on submit", async () => { @@ -115,16 +102,17 @@ describe("ChangeEmailComponent", () => { await component.submit(); - expect(apiService.postEmailToken).not.toHaveBeenCalled(); + expect(changeEmailService.requestEmailToken).not.toHaveBeenCalled(); }); it("sends email token in step 1 if tokenSent is false", async () => { await component.submit(); - expect(apiService.postEmailToken).toHaveBeenCalledWith({ - newEmail: "test@example.com", - masterPasswordHash: "existingHash", - }); + expect(changeEmailService.requestEmailToken).toHaveBeenCalledWith( + "password", + "test@example.com", + "UserId" as UserId, + ); // should activate step 2 expect(component.tokenSent).toBe(true); expect(component.formGroup.controls.step1.disabled).toBe(true); @@ -138,23 +126,6 @@ describe("ChangeEmailComponent", () => { component.formGroup.controls.step1.disable(); component.formGroup.controls.token.enable(); component.formGroup.controls.token.setValue("token"); - - kdfConfigService.getKdfConfig$ - .calledWith("UserId" as any) - .mockReturnValue(of("kdfConfig" as any)); - keyService.userKey$.calledWith("UserId" as any).mockReturnValue(of("userKey" as any)); - - keyService.makeMasterKey - .calledWith("password", "test@example.com", "kdfConfig" as any) - .mockResolvedValue("newMasterKey" as any); - keyService.hashMasterKey - .calledWith("password", "newMasterKey" as any) - .mockResolvedValue("newMasterKeyHash"); - - // Important: make sure this is called with new master key, not existing - keyService.encryptUserKeyWithMasterKey - .calledWith("newMasterKey" as any, "userKey" as any) - .mockResolvedValue(["userKey" as any, { encryptedString: "newEncryptedUserKey" } as any]); }); it("does not post email if token is missing on submit", async () => { @@ -162,38 +133,18 @@ describe("ChangeEmailComponent", () => { await component.submit(); - expect(apiService.postEmail).not.toHaveBeenCalled(); - }); - - it("throws if kdfConfig is missing on submit", async () => { - kdfConfigService.getKdfConfig$.mockReturnValue(of(null)); - - await expect(component.submit()).rejects.toThrow("Missing kdf config"); - }); - - it("throws if userKey can't be found", async () => { - keyService.userKey$.mockReturnValue(of(null)); - - await expect(component.submit()).rejects.toThrow("Can't find UserKey"); - }); - - it("throws if encryptedUserKey is missing", async () => { - keyService.encryptUserKeyWithMasterKey.mockResolvedValue(["userKey" as any, null as any]); - - await expect(component.submit()).rejects.toThrow("Missing Encrypted User Key"); + expect(changeEmailService.confirmEmailChange).not.toHaveBeenCalled(); }); it("submits if step 2 is valid", async () => { await component.submit(); - // validate that hashes are correct - expect(apiService.postEmail).toHaveBeenCalledWith({ - masterPasswordHash: "existingHash", - newMasterPasswordHash: "newMasterKeyHash", - token: "token", - newEmail: "test@example.com", - key: "newEncryptedUserKey", - }); + expect(changeEmailService.confirmEmailChange).toHaveBeenCalledWith( + "password", + "test@example.com", + "token", + "UserId" as UserId, + ); }); }); }); diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index 3daf2240fb2..af7264f45f6 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -2,18 +2,16 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; -import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request"; -import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ChangeEmailService } from "@bitwarden/common/auth/services/change-email/change-email.service"; import { TwoFactorService } from "@bitwarden/common/auth/two-factor"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; @@ -39,14 +37,12 @@ export class ChangeEmailComponent implements OnInit { constructor( private accountService: AccountService, - private apiService: ApiService, private twoFactorService: TwoFactorService, private i18nService: I18nService, - private keyService: KeyService, private messagingService: MessagingService, private formBuilder: FormBuilder, - private kdfConfigService: KdfConfigService, private toastService: ToastService, + private changeEmailService: ChangeEmailService, ) {} async ngOnInit() { @@ -79,53 +75,25 @@ export class ChangeEmailComponent implements OnInit { const newEmail = step1Value.newEmail?.trim().toLowerCase(); const masterPassword = step1Value.masterPassword; - if (newEmail == null || masterPassword == null) { - throw new Error("Missing email or password"); - } - - const existingHash = await this.keyService.hashMasterKey( - masterPassword, - await this.keyService.getOrDeriveMasterKey(masterPassword, this.userId), - ); + const ctx = "Could not update email."; + assertNonNullish(newEmail, "email", ctx); + assertNonNullish(masterPassword, "password", ctx); if (!this.tokenSent) { - const request = new EmailTokenRequest(); - request.newEmail = newEmail; - request.masterPasswordHash = existingHash; - await this.apiService.postEmailToken(request); + await this.changeEmailService.requestEmailToken(masterPassword, newEmail, this.userId); this.activateStep2(); } else { const token = this.formGroup.value.token; if (token == null) { throw new Error("Missing token"); } - const request = new EmailRequest(); - request.token = token; - request.newEmail = newEmail; - request.masterPasswordHash = existingHash; - const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId)); - if (kdfConfig == null) { - throw new Error("Missing kdf config"); - } - const newMasterKey = await this.keyService.makeMasterKey(masterPassword, newEmail, kdfConfig); - request.newMasterPasswordHash = await this.keyService.hashMasterKey( + await this.changeEmailService.confirmEmailChange( masterPassword, - newMasterKey, + newEmail, + token, + this.userId, ); - - const userKey = await firstValueFrom(this.keyService.userKey$(this.userId)); - if (userKey == null) { - throw new Error("Can't find UserKey"); - } - const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey, userKey); - const encryptedUserKey = newUserKey[1]?.encryptedString; - if (encryptedUserKey == null) { - throw new Error("Missing Encrypted User Key"); - } - request.key = encryptedUserKey; - - await this.apiService.postEmail(request); this.reset(); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/auth/settings/account/profile.component.html b/apps/web/src/app/auth/settings/account/profile.component.html index 8ef79f59851..8b0cf6a342c 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.html +++ b/apps/web/src/app/auth/settings/account/profile.component.html @@ -1,10 +1,9 @@
- - {{ "loading" | i18n }} +
@@ -32,8 +31,8 @@ appStopProp [bitAction]="openChangeAvatar" > - - Customize + + {{ "customize" | i18n }}
@@ -43,7 +42,7 @@ rel="noopener noreferrer" href="https://bitwarden.com/help/claimed-accounts" > - +
@if (fingerprintMaterial && userPublicKey) { diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index fd96f343b3a..24e8a370e2a 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -18,8 +18,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component"; +import { AccountFingerprintComponent } from "../../../key-management/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "../../../shared"; -import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html index 1c04c03a8d2..0ba4b29690b 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html @@ -25,7 +25,7 @@ href="https://bitwarden.com/help/emergency-access/#user-access" slot="end" > - + diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 4f3d58608e2..2ce33345337 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -30,7 +30,7 @@ [bitAction]="invite" [disabled]="!(canAccessPremium$ | async)" > - + {{ "addEmergencyContact" | i18n }}
@@ -99,7 +99,7 @@ *ngIf="c.status === emergencyAccessStatusType.Invited" (click)="reinvite(c)" > - + {{ "resendInvitation" | i18n }} @@ -144,12 +144,11 @@

{{ "noTrustedContacts" | i18n }}

- - {{ "loading" | i18n }} +
@@ -221,7 +220,7 @@ *ngIf="c.status === emergencyAccessStatusType.Confirmed" (click)="requestAccess(c)" > - + {{ "requestAccess" | i18n }} @@ -260,12 +259,11 @@

{{ "noGrantedAccess" | i18n }}

- - {{ "loading" | i18n }} +
diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html index 2e0a81da976..e5b81671d7f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.html @@ -7,12 +7,11 @@
@if (initializing) {
- - {{ "loading" | i18n }} +
} @else { diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts index 743f41537e9..09b6934d2d2 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts @@ -21,6 +21,7 @@ import { DialogModule, DialogRef, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -59,6 +60,7 @@ export type EmergencyAccessTakeoverDialogResultType = CommonModule, DialogModule, I18nPipe, + IconModule, InputPasswordComponent, ], }) diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html index 4aaac6aaa52..6c9637ab921 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.html @@ -18,22 +18,20 @@ >{{ currentCipher.name }} - - {{ "shared" | i18n }} + [ariaLabel]="'shared' | i18n" + > - - {{ "attachments" | i18n }} + [ariaLabel]="'attachments' | i18n" + >
{{ currentCipher.subTitle }} @@ -43,11 +41,10 @@ - - {{ "loading" | i18n }} +
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html index 1595c0350d0..a2f39ea3c21 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.html @@ -73,7 +73,7 @@

- +

{{ "twoStepAuthenticatorQRCanvasError" | i18n }}

diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts index d27e8ffecce..0c512b7b99a 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts @@ -29,6 +29,7 @@ import { DialogRef, DialogService, FormFieldModule, + IconModule, SvgModule, InputModule, LinkModule, @@ -63,6 +64,7 @@ declare global { ReactiveFormsModule, DialogModule, FormFieldModule, + IconModule, InputModule, LinkModule, TypographyModule, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html index 8a538cb961c..31ac01fe1da 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.html @@ -17,7 +17,7 @@
  • - + {{ k.name || ("unnamedKey" | i18n) }} @@ -27,12 +27,12 @@ - + > - {{ "remove" | i18n }} @@ -68,19 +68,27 @@ {{ "readKey" | i18n }} - + - + {{ "twoFactorU2fWaiting" | i18n }}... - + {{ "twoFactorU2fClickSave" | i18n }} - + {{ "twoFactorU2fProblemReadingTryAgain" | i18n }} diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts index 57001acc4d2..22cc6e9d9b3 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts @@ -27,6 +27,7 @@ import { DialogRef, DialogService, FormFieldModule, + IconModule, LinkModule, ToastService, TypographyModule, @@ -56,6 +57,7 @@ interface Key { DialogModule, FormFieldModule, I18nPipe, + IconModule, JslibModule, LinkModule, ReactiveFormsModule, diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html index 69a0dbf4145..77c410e8ec6 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.html @@ -34,12 +34,11 @@

    {{ "providers" | i18n }} - - {{ "loading" | i18n }} +

    @@ -59,12 +58,11 @@ {{ p.name }}
- - {{ "enabled" | i18n }} + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html index 147fc9874dd..910da970ff0 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.html @@ -8,7 +8,11 @@
- + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html index 3fe6f43a052..85fa35ed4da 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.html @@ -8,7 +8,11 @@
- + diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index 2ef177922a9..dd260848f52 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -21,7 +21,7 @@ - +

@@ -36,7 +36,7 @@ {{ credential.name }} - + {{ "usedForEncryption" | i18n }} @@ -47,7 +47,7 @@ [attr.aria-label]="('enablePasskeyEncryption' | i18n) + ' ' + credential.name" (click)="enableEncryption(credential.id)" > - + {{ "enablePasskeyEncryption" | i18n }} diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html index 0770ea4dfe1..1f16fe817e1 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.html @@ -21,7 +21,7 @@ {{ "sendCode" | i18n }} - + {{ "codeSent" | i18n }} diff --git a/apps/web/src/app/auth/verify-email-token.component.html b/apps/web/src/app/auth/verify-email-token.component.html index 63437352e19..47e0d0f1517 100644 --- a/apps/web/src/app/auth/verify-email-token.component.html +++ b/apps/web/src/app/auth/verify-email-token.component.html @@ -1,6 +1,10 @@

Bitwarden
- +
diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index fe70f876bc4..f05f9c08b76 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -12,11 +12,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; +import { SharedModule } from "../shared/shared.module"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email-token", templateUrl: "verify-email-token.component.html", + imports: [SharedModule], }) export class VerifyEmailTokenComponent implements OnInit { constructor( diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html index e182659acbb..095c721d9d8 100644 --- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html +++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html @@ -1,5 +1,5 @@
- +
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts index 7e219c44d90..8c7a407eee5 100644 --- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts @@ -3,6 +3,7 @@ import { Component, DestroyRef, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { + catchError, combineLatest, firstValueFrom, from, @@ -32,6 +33,7 @@ import { } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; import { I18nPipe } from "@bitwarden/ui-common"; +import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types"; import { @@ -63,11 +65,12 @@ const RouteParamValues = { I18nPipe, PricingCardComponent, ], + providers: [AccountBillingClient], }) export class CloudHostedPremiumComponent { protected hasPremiumFromAnyOrganization$: Observable; protected hasPremiumPersonally$: Observable; - protected shouldShowNewDesign$: Observable; + protected hasSubscription$: Observable; protected shouldShowUpgradeDialogOnInit$: Observable; protected personalPricingTiers$: Observable; protected premiumCardData$: Observable<{ @@ -84,6 +87,7 @@ export class CloudHostedPremiumComponent { private destroyRef = inject(DestroyRef); constructor( + private accountBillingClient: AccountBillingClient, private accountService: AccountService, private apiService: ApiService, private dialogService: DialogService, @@ -109,27 +113,32 @@ export class CloudHostedPremiumComponent { ), ); + this.hasSubscription$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + account + ? from(this.accountBillingClient.getSubscription()).pipe( + map((subscription) => !!subscription), + catchError(() => of(false)), + ) + : of(false), + ), + ); + this.accountService.activeAccount$ .pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef)) .subscribe((subscriber) => { this.subscriber = subscriber; }); - this.shouldShowNewDesign$ = combineLatest([ - this.hasPremiumFromAnyOrganization$, - this.hasPremiumPersonally$, - ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium)); - - // redirect to user subscription page if they already have premium personally - // redirect to individual vault if they already have premium from an org - combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$]) + combineLatest([this.hasSubscription$, this.hasPremiumFromAnyOrganization$]) .pipe( takeUntilDestroyed(this.destroyRef), - switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => { - if (hasPremiumPersonally) { + take(1), + switchMap(([hasSubscription, hasPremiumFromAnyOrganization]) => { + if (hasSubscription) { return from(this.navigateToSubscriptionPage()); } - if (hasPremiumFromOrg) { + if (hasPremiumFromAnyOrganization) { return from(this.navigateToIndividualVault()); } return of(true); diff --git a/apps/web/src/app/billing/individual/subscription.component.html b/apps/web/src/app/billing/individual/subscription.component.html index 7fd7beff109..cf9344996cb 100644 --- a/apps/web/src/app/billing/individual/subscription.component.html +++ b/apps/web/src/app/billing/individual/subscription.component.html @@ -1,9 +1,10 @@ @if (!selfHosted) { - {{ - "subscription" | i18n - }} + {{ "subscription" | i18n }} {{ "paymentDetails" | i18n }} {{ "billingHistory" | i18n }} diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index 4f52f3c2ea2..454a4d6aa6d 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -19,7 +19,7 @@ import { AccountBillingClient } from "../clients/account-billing.client"; providers: [AccountBillingClient], }) export class SubscriptionComponent implements OnInit { - hasPremium$: Observable; + showSubscriptionPageLink$: Observable; selfHosted: boolean; constructor( @@ -27,9 +27,9 @@ export class SubscriptionComponent implements OnInit { billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, configService: ConfigService, - private accountBillingClient: AccountBillingClient, + accountBillingClient: AccountBillingClient, ) { - this.hasPremium$ = combineLatest([ + this.showSubscriptionPageLink$ = combineLatest([ configService.getFeatureFlag$(FeatureFlag.PM29594_UpdateIndividualSubscriptionPage), accountService.activeAccount$, ]).pipe( diff --git a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts index 7fdc830effd..ed3ac56c968 100644 --- a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts @@ -89,18 +89,34 @@ export class AccountSubscriptionComponent { { initialValue: false }, ); + readonly hasPremiumFromAnyOrganization = toSignal( + this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (!account) { + return of(false); + } + return this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id); + }), + ), + { initialValue: false }, + ); + readonly subscription = resource({ - loader: async () => { - const redirectToPremiumPage = async (): Promise => { + params: () => ({ + account: this.account(), + }), + loader: async ({ params: { account } }) => { + if (!account) { await this.router.navigate(["/settings/subscription/premium"]); return null; - }; - if (!this.account()) { - return await redirectToPremiumPage(); } const subscription = await this.accountBillingClient.getSubscription(); if (!subscription) { - return await redirectToPremiumPage(); + const hasPremiumFromAnyOrganization = this.hasPremiumFromAnyOrganization(); + await this.router.navigate([ + hasPremiumFromAnyOrganization ? "/vault" : "/settings/subscription/premium", + ]); + return null; } return subscription; }, diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html index 610f30f8465..0e58ba56382 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.html @@ -1,5 +1,4 @@ - @let changingPayment = isChangingPaymentMethod(); {{ upgradeToMessage() }} @@ -17,20 +16,18 @@
- @if (!changingPayment) { -
{{ "billingAddress" | i18n }}
- - - } +
{{ "billingAddress" | i18n }}
+ +
@@ -46,7 +43,7 @@ bitButton bitFormButton buttonType="primary" - [disabled]="loading() || !formGroup.valid" + [disabled]="loading() || !isFormValid()" type="submit" > {{ "upgrade" | i18n }} diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.spec.ts index a6b8354f858..8bd965a95d9 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.spec.ts @@ -1,13 +1,6 @@ -import { - Component, - input, - ChangeDetectionStrategy, - CUSTOM_ELEMENTS_SCHEMA, - signal, - output, -} from "@angular/core"; +import { Component, input, ChangeDetectionStrategy, signal, output } from "@angular/core"; import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { FormControl, FormGroup } from "@angular/forms"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -34,6 +27,7 @@ import { SubscriberBillingClient } from "../../../clients/subscriber-billing.cli import { EnterBillingAddressComponent, DisplayPaymentMethodInlineComponent, + EnterPaymentMethodComponent, } from "../../../payment/components"; import { @@ -46,8 +40,7 @@ import { PremiumOrgUpgradeService } from "./services/premium-org-upgrade.service @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: "billing-cart-summary", - template: `

Mock Cart Summary

`, - providers: [{ provide: CartSummaryComponent, useClass: MockCartSummaryComponent }], + template: "", }) class MockCartSummaryComponent { readonly cart = input.required(); @@ -59,52 +52,17 @@ class MockCartSummaryComponent { @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: "app-display-payment-method-inline", - template: `

Mock Display Payment Method

`, - providers: [ - { - provide: DisplayPaymentMethodInlineComponent, - useClass: MockDisplayPaymentMethodInlineComponent, - }, - ], + template: "", }) class MockDisplayPaymentMethodInlineComponent { readonly subscriber = input.required(); readonly paymentMethod = input(); + readonly externalFormGroup = input(); readonly updated = output(); readonly changePaymentMethodClicked = output(); -} -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: "app-enter-billing-address", - template: `

Mock Enter Billing Address

`, - providers: [ - { - provide: EnterBillingAddressComponent, - useClass: MockEnterBillingAddressComponent, - }, - ], -}) -class MockEnterBillingAddressComponent { - readonly scenario = input.required(); - readonly group = input.required(); - - static getFormGroup = () => - new FormGroup({ - country: new FormControl("", { - nonNullable: true, - validators: [Validators.required], - }), - postalCode: new FormControl("", { - nonNullable: true, - validators: [Validators.required], - }), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), - taxId: new FormControl(null), - }); + isChangingPayment = jest.fn().mockReturnValue(false); + getTokenizedPaymentMethod = jest.fn().mockResolvedValue({ token: "test-token" }); } describe("PremiumOrgUpgradePaymentComponent", () => { @@ -169,15 +127,14 @@ describe("PremiumOrgUpgradePaymentComponent", () => { beforeEach(async () => { jest.clearAllMocks(); - mockAccountBillingClient.upgradePremiumToOrganization.mockResolvedValue(undefined); - mockPremiumOrgUpgradeService.upgradeToOrganization.mockResolvedValue(undefined); - mockPremiumOrgUpgradeService.previewProratedInvoice.mockResolvedValue({ - tax: 5.0, - total: 53.0, - credit: 10.0, - newPlanProratedMonths: 1, - }); - mockOrganizationService.organizations$.mockReturnValue(of([])); + + // Set up minimal mocks needed for component initialization + mockSubscriptionPricingService.getBusinessSubscriptionPricingTiers$.mockReturnValue( + of([mockTeamsPlan]), + ); + mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue( + of([mockFamiliesPlan]), + ); mockAccountService.activeAccount$ = of(mockAccount); mockSubscriberBillingClient.getPaymentMethod.mockResolvedValue({ type: "card", @@ -185,18 +142,46 @@ describe("PremiumOrgUpgradePaymentComponent", () => { last4: "4242", expiration: "12/2025", }); + mockOrganizationService.organizations$.mockReturnValue(of([])); + mockPremiumOrgUpgradeService.previewProratedInvoice.mockResolvedValue({ + tax: 5.0, + total: 53.0, + credit: 10.0, + newPlanProratedMonths: 1, + }); - mockSubscriptionPricingService.getBusinessSubscriptionPricingTiers$.mockReturnValue( - of([mockTeamsPlan]), + // Mock static form group methods (required for component creation) + jest.spyOn(EnterPaymentMethodComponent, "getFormGroup").mockReturnValue( + new FormGroup({ + type: new FormControl("card", { nonNullable: true }), + bankAccount: new FormGroup({ + routingNumber: new FormControl("", { nonNullable: true }), + accountNumber: new FormControl("", { nonNullable: true }), + accountHolderName: new FormControl("", { nonNullable: true }), + accountHolderType: new FormControl("", { nonNullable: true }), + }), + billingAddress: new FormGroup({ + country: new FormControl("", { nonNullable: true }), + postalCode: new FormControl("", { nonNullable: true }), + }), + }) as any, ); - mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue( - of([mockFamiliesPlan]), + + jest.spyOn(EnterBillingAddressComponent, "getFormGroup").mockReturnValue( + new FormGroup({ + country: new FormControl("", { nonNullable: true }), + postalCode: new FormControl("", { nonNullable: true }), + line1: new FormControl(null), + line2: new FormControl(null), + city: new FormControl(null), + state: new FormControl(null), + taxId: new FormControl(null), + }), ); await TestBed.configureTestingModule({ imports: [PremiumOrgUpgradePaymentComponent], providers: [ - { provide: PremiumOrgUpgradeService, useValue: mockPremiumOrgUpgradeService }, { provide: SubscriptionPricingServiceAbstraction, useValue: mockSubscriptionPricingService, @@ -209,33 +194,23 @@ describe("PremiumOrgUpgradePaymentComponent", () => { { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, { provide: AccountService, useValue: mockAccountService }, { provide: ApiService, useValue: mockApiService }, + { provide: OrganizationService, useValue: mockOrganizationService }, { provide: KeyService, - useValue: { - makeOrgKey: jest.fn().mockResolvedValue(["encrypted-key", "decrypted-key"]), - }, + useValue: { makeOrgKey: jest.fn().mockResolvedValue(["encrypted-key", "decrypted-key"]) }, }, - { - provide: SyncService, - useValue: { fullSync: jest.fn().mockResolvedValue(undefined) }, - }, - { provide: OrganizationService, useValue: mockOrganizationService }, + { provide: SyncService, useValue: { fullSync: jest.fn().mockResolvedValue(undefined) } }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(PremiumOrgUpgradePaymentComponent, { - add: { - imports: [ - MockEnterBillingAddressComponent, - MockDisplayPaymentMethodInlineComponent, - MockCartSummaryComponent, - ], - }, remove: { - imports: [ - EnterBillingAddressComponent, - DisplayPaymentMethodInlineComponent, - CartSummaryComponent, + imports: [DisplayPaymentMethodInlineComponent, CartSummaryComponent], + providers: [PremiumOrgUpgradeService], + }, + add: { + imports: [MockDisplayPaymentMethodInlineComponent, MockCartSummaryComponent], + providers: [ + { provide: PremiumOrgUpgradeService, useValue: mockPremiumOrgUpgradeService }, ], }, }) @@ -248,7 +223,6 @@ describe("PremiumOrgUpgradePaymentComponent", () => { fixture.componentRef.setInput("account", mockAccount); fixture.detectChanges(); - // Wait for ngOnInit to complete await fixture.whenStable(); }); @@ -262,53 +236,66 @@ describe("PremiumOrgUpgradePaymentComponent", () => { expect(component["upgradeToMessage"]()).toContain("upgradeToTeams"); }); - it("should handle invalid plan id that doesn't exist in pricing tiers", async () => { - // Create a fresh component with an invalid plan ID from the start - const newFixture = TestBed.createComponent(PremiumOrgUpgradePaymentComponent); - const newComponent = newFixture.componentInstance; + describe("Component Initialization with Different Plans", () => { + it("should handle invalid plan id that doesn't exist in pricing tiers", async () => { + // Create a fresh component with an invalid plan ID from the start + const newFixture = TestBed.createComponent(PremiumOrgUpgradePaymentComponent); + const newComponent = newFixture.componentInstance; - newFixture.componentRef.setInput( - "selectedPlanId", - "non-existent-plan" as BusinessSubscriptionPricingTierId, - ); - newFixture.componentRef.setInput("account", mockAccount); - newFixture.detectChanges(); + newFixture.componentRef.setInput( + "selectedPlanId", + "non-existent-plan" as BusinessSubscriptionPricingTierId, + ); + newFixture.componentRef.setInput("account", mockAccount); + newFixture.detectChanges(); - await newFixture.whenStable(); + await newFixture.whenStable(); - expect(newComponent["selectedPlan"]()).toBeNull(); + expect(newComponent["selectedPlan"]()).toBeNull(); + }); + + it("should handle invoice preview errors gracefully", fakeAsync(() => { + mockPremiumOrgUpgradeService.previewProratedInvoice.mockRejectedValue( + new Error("Network error"), + ); + + // Component should still render and be usable even when invoice preview fails + fixture = TestBed.createComponent(PremiumOrgUpgradePaymentComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput("selectedPlanId", "teams" as BusinessSubscriptionPricingTierId); + fixture.componentRef.setInput("account", mockAccount); + fixture.detectChanges(); + + expect(component).toBeTruthy(); + expect(component["selectedPlan"]()).not.toBeNull(); + expect(mockToastService.showToast).not.toHaveBeenCalled(); + })); }); - it("should handle invoice preview errors gracefully", fakeAsync(() => { - mockPremiumOrgUpgradeService.previewProratedInvoice.mockRejectedValue( - new Error("Network error"), - ); - - // Component should still render and be usable even when invoice preview fails - fixture = TestBed.createComponent(PremiumOrgUpgradePaymentComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput("selectedPlanId", "teams" as BusinessSubscriptionPricingTierId); - fixture.componentRef.setInput("account", mockAccount); - fixture.detectChanges(); - tick(); - - expect(component).toBeTruthy(); - expect(component["selectedPlan"]()).not.toBeNull(); - expect(mockToastService.showToast).not.toHaveBeenCalled(); - })); - describe("submit", () => { + beforeEach(() => { + // Set up upgrade service mock for submit tests + mockPremiumOrgUpgradeService.upgradeToOrganization.mockResolvedValue("new-org-id"); + }); + it("should successfully upgrade to organization", async () => { const completeSpy = jest.spyOn(component["complete"], "emit"); - // Mock processUpgrade to bypass form validation - jest.spyOn(component as any, "processUpgrade").mockResolvedValue({ - status: PremiumOrgUpgradePaymentStatus.UpgradedToTeams, - organizationId: null, - }); - component["formGroup"].setValue({ organizationName: "My New Org", + paymentMethodForm: { + type: "card", + bankAccount: { + routingNumber: "", + accountNumber: "", + accountHolderName: "", + accountHolderType: "", + }, + billingAddress: { + country: "", + postalCode: "", + }, + }, billingAddress: { country: "US", postalCode: "90210", @@ -322,13 +309,25 @@ describe("PremiumOrgUpgradePaymentComponent", () => { await component["submit"](); + expect(mockPremiumOrgUpgradeService.upgradeToOrganization).toHaveBeenCalledWith( + mockAccount, + "My New Org", + component["selectedPlan"](), + expect.objectContaining({ + country: "US", + postalCode: "90210", + line1: "123 Main St", + city: "Beverly Hills", + state: "CA", + }), + ); expect(mockToastService.showToast).toHaveBeenCalledWith({ variant: "success", message: "plansUpdated", }); expect(completeSpy).toHaveBeenCalledWith({ status: PremiumOrgUpgradePaymentStatus.UpgradedToTeams, - organizationId: null, + organizationId: "new-org-id", }); }); @@ -340,6 +339,19 @@ describe("PremiumOrgUpgradePaymentComponent", () => { component["formGroup"].setValue({ organizationName: "My New Org", + paymentMethodForm: { + type: "card", + bankAccount: { + routingNumber: "", + accountNumber: "", + accountHolderName: "", + accountHolderType: "", + }, + billingAddress: { + country: "", + postalCode: "", + }, + }, billingAddress: { country: "US", postalCode: "90210", @@ -469,7 +481,8 @@ describe("PremiumOrgUpgradePaymentComponent", () => { describe("processUpgrade", () => { beforeEach(() => { - // Set paymentMethod signal for these tests + // Set up mocks specific to processUpgrade tests + mockPremiumOrgUpgradeService.upgradeToOrganization.mockResolvedValue("org-id-123"); component["paymentMethod"].set({ type: "card", brand: "visa", @@ -501,6 +514,133 @@ describe("PremiumOrgUpgradePaymentComponent", () => { await expect(component["processUpgrade"]()).rejects.toThrow("Organization name is required"); }); + + it("should update payment method when isChangingPayment returns true", async () => { + const mockPaymentMethodComponent = { + isChangingPayment: jest.fn().mockReturnValue(true), + getTokenizedPaymentMethod: jest.fn().mockResolvedValue({ token: "new-token-123" }), + }; + jest + .spyOn(component, "paymentMethodComponent") + .mockReturnValue(mockPaymentMethodComponent as any); + + const mockSubscriber = { id: "subscriber-123" }; + component["subscriber"].set(mockSubscriber as any); + component["selectedPlan"].set({ + tier: "teams" as BusinessSubscriptionPricingTierId, + details: mockTeamsPlan, + cost: 48, + }); + + component["formGroup"].patchValue({ + organizationName: "Test Organization", + billingAddress: { + country: "US", + postalCode: "12345", + }, + }); + + const result = await component["processUpgrade"](); + + expect(mockPaymentMethodComponent.isChangingPayment).toHaveBeenCalled(); + expect(mockPaymentMethodComponent.getTokenizedPaymentMethod).toHaveBeenCalled(); + expect(mockSubscriberBillingClient.updatePaymentMethod).toHaveBeenCalledWith( + mockSubscriber, + { token: "new-token-123" }, + expect.objectContaining({ + country: "US", + postalCode: "12345", + }), + ); + expect(mockPremiumOrgUpgradeService.upgradeToOrganization).toHaveBeenCalledWith( + mockAccount, + "Test Organization", + expect.objectContaining({ tier: "teams" }), + expect.objectContaining({ + country: "US", + postalCode: "12345", + }), + ); + expect(result.organizationId).toBe("org-id-123"); + }); + + it("should not update payment method when isChangingPayment returns false", async () => { + const mockPaymentMethodComponent = { + isChangingPayment: jest.fn().mockReturnValue(false), + getTokenizedPaymentMethod: jest.fn(), + }; + jest + .spyOn(component, "paymentMethodComponent") + .mockReturnValue(mockPaymentMethodComponent as any); + component["selectedPlan"].set({ + tier: "teams" as BusinessSubscriptionPricingTierId, + details: mockTeamsPlan, + cost: 48, + }); + + component["formGroup"].patchValue({ + organizationName: "Test Organization", + billingAddress: { + country: "US", + postalCode: "12345", + }, + }); + + await component["processUpgrade"](); + + expect(mockPaymentMethodComponent.isChangingPayment).toHaveBeenCalled(); + expect(mockPaymentMethodComponent.getTokenizedPaymentMethod).not.toHaveBeenCalled(); + expect(mockSubscriberBillingClient.updatePaymentMethod).not.toHaveBeenCalled(); + expect(mockPremiumOrgUpgradeService.upgradeToOrganization).toHaveBeenCalled(); + }); + + it("should handle null paymentMethodComponent gracefully", async () => { + jest.spyOn(component, "paymentMethodComponent").mockReturnValue(null as any); + component["selectedPlan"].set({ + tier: "teams" as BusinessSubscriptionPricingTierId, + details: mockTeamsPlan, + cost: 48, + }); + + component["formGroup"].patchValue({ + organizationName: "Test Organization", + billingAddress: { + country: "US", + postalCode: "12345", + }, + }); + + await component["processUpgrade"](); + + expect(mockSubscriberBillingClient.updatePaymentMethod).not.toHaveBeenCalled(); + expect(mockPremiumOrgUpgradeService.upgradeToOrganization).toHaveBeenCalled(); + }); + + it("should throw error when payment method is null and user is not changing payment", async () => { + const mockPaymentMethodComponent = { + isChangingPayment: jest.fn().mockReturnValue(false), + getTokenizedPaymentMethod: jest.fn(), + }; + jest + .spyOn(component, "paymentMethodComponent") + .mockReturnValue(mockPaymentMethodComponent as any); + component["paymentMethod"].set(null); + component["selectedPlan"].set({ + tier: "teams" as BusinessSubscriptionPricingTierId, + details: mockTeamsPlan, + cost: 48, + }); + + component["formGroup"].patchValue({ + organizationName: "Test Organization", + billingAddress: { + country: "US", + postalCode: "12345", + }, + }); + + await expect(component["processUpgrade"]()).rejects.toThrow("Payment method is required"); + }); }); describe("Plan Membership Messages", () => { @@ -545,6 +685,19 @@ describe("PremiumOrgUpgradePaymentComponent", () => { component["formGroup"].setValue({ organizationName: "My New Org", + paymentMethodForm: { + type: "card", + bankAccount: { + routingNumber: "", + accountNumber: "", + accountHolderName: "", + accountHolderType: "", + }, + billingAddress: { + country: "", + postalCode: "", + }, + }, billingAddress: { country: "US", postalCode: "90210", @@ -573,4 +726,25 @@ describe("PremiumOrgUpgradePaymentComponent", () => { expect(goBackSpy).toHaveBeenCalled(); }); }); + + describe("Payment Method Initialization", () => { + it("should set subscriber and payment method signals on init", async () => { + const subscriber = component["subscriber"](); + expect(subscriber).toEqual( + expect.objectContaining({ + type: "account", + data: expect.objectContaining({ + id: mockAccount.id, + email: mockAccount.email, + }), + }), + ); + expect(component["paymentMethod"]()).toEqual({ + type: "card", + brand: "visa", + last4: "4242", + expiration: "12/2025", + }); + }); + }); }); diff --git a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts index 4a7207686b8..ab4c4177846 100644 --- a/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/premium-org-upgrade-payment/premium-org-upgrade-payment.component.ts @@ -24,7 +24,6 @@ import { from, defer, map, - tap, } from "rxjs"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -48,6 +47,7 @@ import { EnterBillingAddressComponent, getBillingAddressFromForm, DisplayPaymentMethodInlineComponent, + EnterPaymentMethodComponent, } from "../../../payment/components"; import { MaskedPaymentMethod } from "../../../payment/types"; import { BitwardenSubscriber, mapAccountToSubscriber } from "../../../types"; @@ -110,13 +110,17 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit PersonalSubscriptionPricingTierId | BusinessSubscriptionPricingTierId >(); protected readonly account = input.required(); + protected goBack = output(); protected complete = output(); - readonly cartSummaryComponent = viewChild.required(CartSummaryComponent); + readonly cartSummaryComponent = viewChild.required("cartSummaryComponent"); + readonly paymentMethodComponent = + viewChild.required("paymentMethodComponent"); protected formGroup = new FormGroup({ organizationName: new FormControl("", [Validators.required]), + paymentMethodForm: EnterPaymentMethodComponent.getFormGroup(), billingAddress: EnterBillingAddressComponent.getFormGroup(), }); @@ -127,12 +131,6 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit // Signals for payment method protected readonly paymentMethod = signal(null); protected readonly subscriber = signal(null); - /** - * Indicates whether the payment method is currently being changed. - * This is used to disable the submit button while a payment method change is in progress. - * or to hide other UI elements as needed. - */ - protected readonly isChangingPaymentMethod = signal(false); protected readonly planMembershipMessage = computed( () => this.PLAN_MEMBERSHIP_MESSAGES[this.selectedPlanId()] ?? "", @@ -252,14 +250,13 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit map((paymentMethod) => ({ subscriber, paymentMethod })), ), ), - tap(({ subscriber, paymentMethod }) => { - this.subscriber.set(subscriber); - this.paymentMethod.set(paymentMethod); - this.loading.set(false); - }), takeUntilDestroyed(this.destroyRef), ) - .subscribe(); + .subscribe(({ subscriber, paymentMethod }) => { + this.subscriber.set(subscriber); + this.paymentMethod.set(paymentMethod); + this.loading.set(false); + }); } ngAfterViewInit(): void { @@ -267,24 +264,8 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit cartSummaryComponent.isExpanded.set(false); } - /** - * Updates the payment method when changed through the DisplayPaymentMethodComponent. - * @param newPaymentMethod The updated payment method details - */ - handlePaymentMethodUpdate(newPaymentMethod: MaskedPaymentMethod) { - this.paymentMethod.set(newPaymentMethod); - } - - /** - * Handles changes to the payment method changing state. - * @param isChanging Whether the payment method is currently being changed - */ - handlePaymentMethodChangingStateChange(isChanging: boolean) { - this.isChangingPaymentMethod.set(isChanging); - } - protected submit = async (): Promise => { - if (!this.formGroup.valid) { + if (!this.isFormValid()) { this.formGroup.markAllAsTouched(); return; } @@ -312,11 +293,24 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit private async processUpgrade(): Promise { const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); const organizationName = this.formGroup.value?.organizationName; - if (!billingAddress.country || !billingAddress.postalCode) { throw new Error("Billing address is incomplete"); } + const paymentMethodComponent = this.paymentMethodComponent(); + // If the user is changing their payment method, process that first + if (paymentMethodComponent && paymentMethodComponent.isChangingPayment()) { + const newPaymentMethod = await paymentMethodComponent.getTokenizedPaymentMethod(); + await this.subscriberBillingClient.updatePaymentMethod( + this.subscriber()!, + newPaymentMethod, + billingAddress, + ); + } else if (!this.paymentMethod()) { + // If user is not changing payment method but has no payment method on file + throw new Error("Payment method is required"); + } + if (!organizationName) { throw new Error("Organization name is required"); } @@ -440,11 +434,28 @@ export class PremiumOrgUpgradePaymentComponent implements OnInit, AfterViewInit }; } + /** + * Checks if the form is valid. + */ + protected isFormValid(): boolean { + const isParentFormValid = + this.formGroup.controls.organizationName.valid && + this.formGroup.controls.billingAddress.valid; + + const paymentMethodComponent = this.paymentMethodComponent(); + const isChangingPayment = paymentMethodComponent?.isChangingPayment(); + if (paymentMethodComponent && isChangingPayment) { + return isParentFormValid && paymentMethodComponent.isFormValid(); + } + + return isParentFormValid; + } + /** * Refreshes the invoice preview based on the current form state. */ private refreshInvoicePreview$(): Observable { - if (this.formGroup.invalid || !this.selectedPlan()) { + if (!this.isFormValid()) { return of(this.getEmptyInvoicePreview()); } diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts new file mode 100644 index 00000000000..aa4cbdab40e --- /dev/null +++ b/apps/web/src/app/billing/organizations/organization-plans.component.spec.ts @@ -0,0 +1,2199 @@ +// These are disabled until we can migrate to signals and remove the use of @Input properties that are used within the mocked child components +/* eslint-disable @angular-eslint/prefer-output-emitter-ref */ +/* eslint-disable @angular-eslint/prefer-signals */ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; +import { + PreviewInvoiceClient, + SubscriberBillingClient, +} from "@bitwarden/web-vault/app/billing/clients"; + +import { OrganizationInformationComponent } from "../../admin-console/organizations/create/organization-information.component"; +import { EnterBillingAddressComponent, EnterPaymentMethodComponent } from "../payment/components"; +import { SecretsManagerSubscribeComponent } from "../shared"; +import { OrganizationSelfHostingLicenseUploaderComponent } from "../shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component"; + +import { OrganizationPlansComponent } from "./organization-plans.component"; + +// Mocked Child Components +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-org-info", + template: "", + standalone: true, +}) +class MockOrgInfoComponent { + @Input() formGroup: any; + @Input() createOrganization = true; + @Input() isProvider = false; + @Input() acceptingSponsorship = false; + @Output() changedBusinessOwned = new EventEmitter(); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "sm-subscribe", + template: "", + standalone: true, +}) +class MockSmSubscribeComponent { + @Input() formGroup: any; + @Input() selectedPlan: any; + @Input() upgradeOrganization = false; +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-payment-method", + template: "", + standalone: true, +}) +class MockEnterPaymentMethodComponent { + @Input() group: any; + + static getFormGroup() { + const fb = new FormBuilder(); + return fb.group({ + type: fb.control("card"), + bankAccount: fb.group({ + routingNumber: fb.control(""), + accountNumber: fb.control(""), + accountHolderName: fb.control(""), + accountHolderType: fb.control(""), + }), + billingAddress: fb.group({ + country: fb.control("US"), + postalCode: fb.control(""), + }), + }); + } + + tokenize = jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }); +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-enter-billing-address", + template: "", + standalone: true, +}) +class MockEnterBillingAddressComponent { + @Input() group: any; + @Input() scenario: any; + + static getFormGroup() { + return new FormBuilder().group({ + country: ["US", Validators.required], + postalCode: ["", Validators.required], + taxId: [""], + line1: [""], + line2: [""], + city: [""], + state: [""], + }); + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "organization-self-hosting-license-uploader", + template: "", + standalone: true, +}) +class MockOrganizationSelfHostingLicenseUploaderComponent { + @Output() onLicenseFileUploaded = new EventEmitter(); +} + +// Test Helper Functions + +/** + * Sets up mock encryption keys and org key services + */ +const setupMockEncryptionKeys = ( + mockKeyService: jest.Mocked, + mockEncryptService: jest.Mocked, +) => { + mockKeyService.makeOrgKey.mockResolvedValue([{ encryptedString: "mock-key" }, {} as any] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); +}; + +/** + * Sets up a mock payment method component that returns a successful tokenization + */ +const setupMockPaymentMethodComponent = ( + component: OrganizationPlansComponent, + token = "mock_token", + type = "card", +) => { + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ token, type }), + } as any; +}; + +/** + * Patches billing address form with standard test values + */ +const patchBillingAddress = ( + component: OrganizationPlansComponent, + overrides: Partial<{ + country: string; + postalCode: string; + line1: string; + line2: string; + city: string; + state: string; + taxId: string; + }> = {}, +) => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + line2: "", + city: "City", + state: "CA", + taxId: "", + ...overrides, + }); +}; + +/** + * Sets up a mock organization for upgrade scenarios + */ +const setupMockUpgradeOrganization = ( + mockOrganizationApiService: jest.Mocked, + organizationsSubject: BehaviorSubject, + orgConfig: { + id?: string; + productTierType?: ProductTierType; + hasPaymentSource?: boolean; + planType?: PlanType; + seats?: number; + maxStorageGb?: number; + hasPublicAndPrivateKeys?: boolean; + useSecretsManager?: boolean; + smSeats?: number; + smServiceAccounts?: number; + } = {}, +) => { + const { + id = "org-123", + productTierType = ProductTierType.Free, + hasPaymentSource = true, + planType = PlanType.Free, + seats = 5, + maxStorageGb, + hasPublicAndPrivateKeys = true, + useSecretsManager = false, + smSeats, + smServiceAccounts, + } = orgConfig; + + const mockOrganization = { + id, + name: "Test Org", + productTierType, + seats, + maxStorageGb, + hasPublicAndPrivateKeys, + useSecretsManager, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: hasPaymentSource ? { type: "card" } : null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType, + smSeats, + smServiceAccounts, + } as any); + + return mockOrganization; +}; + +/** + * Patches organization form with basic test values + */ +const patchOrganizationForm = ( + component: OrganizationPlansComponent, + values: { + name?: string; + billingEmail?: string; + productTier?: ProductTierType; + plan?: PlanType; + additionalSeats?: number; + additionalStorage?: number; + }, +) => { + component.formGroup.patchValue({ + name: "Test Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + additionalSeats: 0, + additionalStorage: 0, + ...values, + }); +}; + +/** + * Returns plan details + * + */ + +const createMockPlans = (): PlanResponse[] => { + return [ + { + type: PlanType.Free, + productTier: ProductTierType.Free, + name: "Free", + isAnnual: true, + upgradeSortOrder: 1, + displaySortOrder: 1, + PasswordManager: { + basePrice: 0, + seatPrice: 0, + maxSeats: 2, + baseSeats: 2, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: false, + hasPremiumAccessOption: false, + baseStorageGb: 0, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.FamiliesAnnually, + productTier: ProductTierType.Families, + name: "Families", + isAnnual: true, + upgradeSortOrder: 2, + displaySortOrder: 2, + PasswordManager: { + basePrice: 40, + seatPrice: 0, + maxSeats: 6, + baseSeats: 6, + hasAdditionalSeatsOption: false, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: false, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + }, + SecretsManager: null, + } as PlanResponse, + { + type: PlanType.TeamsAnnually, + productTier: ProductTierType.Teams, + name: "Teams", + isAnnual: true, + canBeUsedByBusiness: true, + upgradeSortOrder: 3, + displaySortOrder: 3, + PasswordManager: { + basePrice: 0, + seatPrice: 48, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 50, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + { + type: PlanType.EnterpriseAnnually, + productTier: ProductTierType.Enterprise, + name: "Enterprise", + isAnnual: true, + canBeUsedByBusiness: true, + trialPeriodDays: 7, + upgradeSortOrder: 4, + displaySortOrder: 4, + PasswordManager: { + basePrice: 0, + seatPrice: 72, + hasAdditionalSeatsOption: true, + hasAdditionalStorageOption: true, + hasPremiumAccessOption: true, + baseStorageGb: 1, + additionalStoragePricePerGb: 4, + premiumAccessOptionPrice: 40, + }, + SecretsManager: { + basePrice: 0, + seatPrice: 144, + hasAdditionalSeatsOption: true, + hasAdditionalServiceAccountOption: true, + baseServiceAccount: 200, + additionalPricePerServiceAccount: 6, + }, + } as PlanResponse, + ]; +}; + +describe("OrganizationPlansComponent", () => { + let component: OrganizationPlansComponent; + let fixture: ComponentFixture; + + // Mock services + let mockApiService: jest.Mocked; + let mockI18nService: jest.Mocked; + let mockPlatformUtilsService: jest.Mocked; + let mockKeyService: jest.Mocked; + let mockEncryptService: jest.Mocked; + let mockRouter: jest.Mocked; + let mockSyncService: jest.Mocked; + let mockPolicyService: jest.Mocked; + let mockOrganizationService: jest.Mocked; + let mockMessagingService: jest.Mocked; + let mockOrganizationApiService: jest.Mocked; + let mockProviderApiService: jest.Mocked; + let mockToastService: jest.Mocked; + let mockAccountService: jest.Mocked; + let mockSubscriberBillingClient: jest.Mocked; + let mockPreviewInvoiceClient: jest.Mocked; + let mockConfigService: jest.Mocked; + + // Mock data + let mockPasswordManagerPlans: PlanResponse[]; + let mockOrganization: Organization; + let activeAccountSubject: BehaviorSubject; + let organizationsSubject: BehaviorSubject; + + beforeEach(async () => { + jest.clearAllMocks(); + + // Mock the static getFormGroup methods to return forms without validators + jest + .spyOn(EnterPaymentMethodComponent, "getFormGroup") + .mockReturnValue(MockEnterPaymentMethodComponent.getFormGroup() as any); + jest + .spyOn(EnterBillingAddressComponent, "getFormGroup") + .mockReturnValue(MockEnterBillingAddressComponent.getFormGroup() as any); + + // Initialize mock services + mockApiService = { + getPlans: jest.fn(), + postProviderCreateOrganization: jest.fn(), + refreshIdentityToken: jest.fn(), + } as any; + + mockI18nService = { + t: jest.fn((key: string) => key), + } as any; + + mockPlatformUtilsService = { + isSelfHost: jest.fn().mockReturnValue(false), + } as any; + + mockKeyService = { + makeOrgKey: jest.fn(), + makeKeyPair: jest.fn(), + orgKeys$: jest.fn().mockReturnValue(of({})), + providerKeys$: jest.fn().mockReturnValue(of({})), + } as any; + + mockEncryptService = { + encryptString: jest.fn(), + wrapSymmetricKey: jest.fn(), + } as any; + + mockRouter = { + navigate: jest.fn(), + } as any; + + mockSyncService = { + fullSync: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPolicyService = { + policyAppliesToUser$: jest.fn().mockReturnValue(of(false)), + } as any; + + // Setup subjects for observables + activeAccountSubject = new BehaviorSubject({ + id: "user-id", + email: "test@example.com", + }); + organizationsSubject = new BehaviorSubject([]); + + mockAccountService = { + activeAccount$: activeAccountSubject.asObservable(), + } as any; + + mockOrganizationService = { + organizations$: jest.fn().mockReturnValue(organizationsSubject.asObservable()), + } as any; + + mockMessagingService = { + send: jest.fn(), + } as any; + + mockOrganizationApiService = { + getBilling: jest.fn(), + getSubscription: jest.fn(), + create: jest.fn(), + createLicense: jest.fn(), + upgrade: jest.fn(), + updateKeys: jest.fn(), + } as any; + + mockProviderApiService = { + getProvider: jest.fn(), + } as any; + + mockToastService = { + showToast: jest.fn(), + } as any; + + mockSubscriberBillingClient = { + getBillingAddress: jest.fn().mockResolvedValue({ + country: "US", + postalCode: "12345", + }), + updatePaymentMethod: jest.fn().mockResolvedValue(undefined), + } as any; + + mockPreviewInvoiceClient = { + previewTaxForOrganizationSubscriptionPurchase: jest.fn().mockResolvedValue({ + tax: 5.0, + total: 50.0, + }), + } as any; + + mockConfigService = { + getFeatureFlag: jest.fn().mockResolvedValue(true), + } as any; + + // Setup mock plan data + mockPasswordManagerPlans = createMockPlans(); + + mockApiService.getPlans.mockResolvedValue({ + data: mockPasswordManagerPlans, + } as any); + + await TestBed.configureTestingModule({ + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: I18nService, useValue: mockI18nService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: KeyService, useValue: mockKeyService }, + { provide: EncryptService, useValue: mockEncryptService }, + { provide: Router, useValue: mockRouter }, + { provide: SyncService, useValue: mockSyncService }, + { provide: PolicyService, useValue: mockPolicyService }, + { provide: OrganizationService, useValue: mockOrganizationService }, + { provide: MessagingService, useValue: mockMessagingService }, + FormBuilder, // Use real FormBuilder + { provide: OrganizationApiServiceAbstraction, useValue: mockOrganizationApiService }, + { provide: ProviderApiServiceAbstraction, useValue: mockProviderApiService }, + { provide: ToastService, useValue: mockToastService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }) + // Override the component to replace child components with mocks and provide mock services + .overrideComponent(OrganizationPlansComponent, { + remove: { + imports: [ + OrganizationInformationComponent, + SecretsManagerSubscribeComponent, + EnterPaymentMethodComponent, + EnterBillingAddressComponent, + OrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [PreviewInvoiceClient, SubscriberBillingClient], + }, + add: { + imports: [ + MockOrgInfoComponent, + MockSmSubscribeComponent, + MockEnterPaymentMethodComponent, + MockEnterBillingAddressComponent, + MockOrganizationSelfHostingLicenseUploaderComponent, + ], + providers: [ + { provide: PreviewInvoiceClient, useValue: mockPreviewInvoiceClient }, + { provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(OrganizationPlansComponent); + component = fixture.componentInstance; + }); + + describe("component creation", () => { + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize with default values", () => { + expect(component.loading).toBe(true); + expect(component.showFree).toBe(true); + expect(component.showCancel).toBe(false); + expect(component.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("ngOnInit", () => { + describe("create organization flow", () => { + it("should load plans from API", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockApiService.getPlans).toHaveBeenCalled(); + expect(component.passwordManagerPlans).toEqual(mockPasswordManagerPlans); + expect(component.loading).toBe(false); + }); + + it("should set required validators on name and billing email", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + expect(component.formGroup.controls.name.hasError("required")).toBe(true); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(true); + }); + + it("should not load organization data for create flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockOrganizationApiService.getBilling).not.toHaveBeenCalled(); + expect(mockOrganizationApiService.getSubscription).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade organization flow", () => { + beforeEach(() => { + mockOrganization = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + planType: PlanType.FamiliesAnnually2025, + }, + ); + + component.organizationId = mockOrganization.id; + }); + + it("should load existing organization data", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.organization).toEqual(mockOrganization); + expect(mockOrganizationApiService.getBilling).toHaveBeenCalledWith(mockOrganization.id); + expect(mockOrganizationApiService.getSubscription).toHaveBeenCalledWith( + mockOrganization.id, + ); + expect(mockSubscriberBillingClient.getBillingAddress).toHaveBeenCalledWith({ + type: "organization", + data: mockOrganization, + }); + // Verify the form was updated + expect(component.billingFormGroup.controls.billingAddress.value.country).toBe("US"); + expect(component.billingFormGroup.controls.billingAddress.value.postalCode).toBe("12345"); + }); + + it("should not add validators for name and billingEmail in upgrade flow", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + + // In upgrade flow, these should not be required + expect(component.formGroup.controls.name.hasError("required")).toBe(false); + expect(component.formGroup.controls.billingEmail.hasError("required")).toBe(false); + }); + }); + + describe("feature flags", () => { + it("should use FamiliesAnnually when PM26462_Milestone_3 is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually); + }); + + it("should use FamiliesAnnually2025 when feature flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + fixture.detectChanges(); + await fixture.whenStable(); + + const familyPlan = component["_familyPlan"]; + expect(familyPlan).toBe(PlanType.FamiliesAnnually2025); + }); + }); + }); + + describe("organization creation validation flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prevent submission with invalid form data", async () => { + component.formGroup.patchValue({ + name: "", + billingEmail: "invalid-email", + additionalStorage: -1, + additionalSeats: 200000, + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.formGroup.invalid).toBe(true); + }); + + it("should allow submission with valid form data", async () => { + patchOrganizationForm(component, { + name: "Valid Organization", + billingEmail: "valid@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); + + describe("plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should configure form appropriately when switching between product tiers", () => { + // Start with Families plan with unsupported features + component.productTier = ProductTierType.Families; + component.formGroup.controls.additionalSeats.setValue(10); + component.formGroup.controls.additionalStorage.setValue(5); + component.changedProduct(); + + // Families doesn't support additional seats + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + expect(component.formGroup.controls.plan.value).toBe(PlanType.FamiliesAnnually); + + // Switch to Teams plan which supports additional seats + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + // Teams initializes with 1 seat by default + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThan(0); + + // Switch to Free plan which doesn't support additional storage + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + }); + + describe("subscription pricing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate total price based on selected plan options", () => { + // Select Teams plan and configure options + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + component.formGroup.controls.premiumAccessAddon.setValue(true); + + const pmSubtotal = component.passwordManagerSubtotal; + // Verify pricing includes all selected options + expect(pmSubtotal).toBeGreaterThan(0); + expect(pmSubtotal).toBe(5 * 48 + 10 * 4 + 40); // seats + storage + premium + }); + + it("should calculate pricing with Secrets Manager addon", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + // Enable Secrets Manager with additional options + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + + // Disable Secrets Manager + component.secretsManagerForm.patchValue({ + enabled: false, + }); + + expect(component.secretsManagerSubtotal).toBe(0); + }); + }); + + describe("tax calculation", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate tax after debounce period", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(1); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + }); + + tick(1500); // Wait for debounce (1000ms) + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + expect(component["estimatedTax"]).toBe(5.0); + })); + + it("should not calculate tax with invalid billing address", fakeAsync(() => { + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + tick(1500); + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).not.toHaveBeenCalled(); + })); + }); + + describe("submit", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should create organization successfully", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(true); + }); + + it("should emit onSuccess after successful creation", async () => { + const onSuccessSpy = jest.spyOn(component.onSuccess, "emit"); + + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + }); + + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + mockOrganizationApiService.create.mockResolvedValue({ + id: "new-org-id", + } as any); + + await component.submit(); + + expect(onSuccessSpy).toHaveBeenCalledWith({ + organizationId: "new-org-id", + }); + }); + + it("should handle payment method validation failure", async () => { + patchOrganizationForm(component, { + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + patchBillingAddress(component); + setupMockEncryptionKeys(mockKeyService, mockEncryptService); + + // Mock payment method component to return null (failure) + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue(null), + } as any; + + await component.submit(); + + // Should not create organization if payment method validation fails + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + + it("should block submission when single org policy applies", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + // Need to reinitialize after changing policy mock + const policyFixture = TestBed.createComponent(OrganizationPlansComponent); + const policyComponent = policyFixture.componentInstance; + policyFixture.detectChanges(); + await policyFixture.whenStable(); + + policyComponent.formGroup.patchValue({ + name: "Test", + billingEmail: "test@example.com", + }); + + await policyComponent.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + }); + }); + + describe("provider flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + }); + + it("should load provider data", async () => { + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(mockProviderApiService.getProvider).toHaveBeenCalledWith("provider-123"); + expect(component.provider).toBeDefined(); + }); + + it("should default to Teams Annual plan for providers", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.plan).toBe(PlanType.TeamsAnnually); + }); + + it("should require clientOwnerEmail for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + const clientOwnerEmailControl = component.formGroup.controls.clientOwnerEmail; + clientOwnerEmailControl.setValue(""); + + expect(clientOwnerEmailControl.hasError("required")).toBe(true); + }); + + it("should set businessOwned to true for provider flow", async () => { + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.formGroup.controls.businessOwned.value).toBe(true); + }); + }); + + describe("self-hosted flow", () => { + beforeEach(async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + }); + + it("should render organization self-hosted license and not load plans", async () => { + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + const selfHostedFixture = TestBed.createComponent(OrganizationPlansComponent); + const selfHostedComponent = selfHostedFixture.componentInstance; + + expect(selfHostedComponent.selfHosted).toBe(true); + expect(mockApiService.getPlans).not.toHaveBeenCalled(); + }); + + it("should handle license file upload success", async () => { + const successSpy = jest.spyOn(component.onSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: "organizationCreated", + message: "organizationReadyToGo", + }); + + expect(successSpy).toHaveBeenCalledWith({ + organizationId: "uploaded-org-id", + }); + + expect(mockMessagingService.send).toHaveBeenCalledWith("organizationCreated", { + organizationId: "uploaded-org-id", + }); + }); + + it("should navigate after license upload if not in trial or sponsorship flow", async () => { + component.acceptingSponsorship = false; + component["isInTrialFlow"] = false; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).toHaveBeenCalledWith(["/organizations/uploaded-org-id"]); + }); + + it("should not navigate after license upload if accepting sponsorship", async () => { + component.acceptingSponsorship = true; + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it("should emit trial success after license upload in trial flow", async () => { + component["isInTrialFlow"] = true; + + fixture.detectChanges(); + await fixture.whenStable(); + + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + await component["onLicenseFileUploaded"]("uploaded-org-id"); + + expect(trialSpy).toHaveBeenCalled(); + }); + }); + + describe("policy enforcement", () => { + it("should check single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyAppliesToActiveUser).toBe(true); + }); + + it("should not block provider flow with single org policy", async () => { + mockPolicyService.policyAppliesToUser$.mockReturnValue(of(true)); + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({} as any); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.singleOrgPolicyBlock).toBe(false); + }); + }); + + describe("business ownership change flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should automatically upgrade to business-compatible plan when marking as business-owned", () => { + // Start with a personal plan + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + // Mark as business-owned + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should automatically switch to Teams (lowest business plan) + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + + // Unchecking businessOwned should not force a downgrade + component.formGroup.controls.businessOwned.setValue(false); + component.changedOwnedBusiness(); + + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + }); + + describe("business organization plan selection flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should restrict available plans based on business ownership and upgrade context", () => { + // Upgrade flow (showFree = false) should exclude Free plan + component.showFree = false; + let products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + + // Create flow (showFree = true) should include Free plan + component.showFree = true; + products = component.selectableProducts; + expect(products.find((p) => p.type === PlanType.Free)).toBeDefined(); + + // Business organizations should only see business-compatible plans + component.formGroup.controls.businessOwned.setValue(true); + products = component.selectableProducts; + const nonFreeBusinessPlans = products.filter((p) => p.type !== PlanType.Free); + nonFreeBusinessPlans.forEach((plan) => { + expect(plan.canBeUsedByBusiness).toBe(true); + }); + }); + }); + + describe("accepting sponsorship flow", () => { + beforeEach(() => { + component.acceptingSponsorship = true; + }); + + it("should configure Families plan with full discount when accepting sponsorship", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + // Only Families plan should be available + const products = component.selectableProducts; + expect(products.length).toBe(1); + expect(products[0].productTier).toBe(ProductTierType.Families); + + // Full discount should be applied making the base price free + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const subtotal = component.passwordManagerSubtotal; + expect(subtotal).toBe(0); // Discount covers the full base price + expect(component.discount).toBe(products[0].PasswordManager.basePrice); + }); + }); + + describe("upgrade flow", () => { + it("should successfully upgrade organization", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + planType: PlanType.TeamsAnnually, + additionalSeats: 5, + }), + ); + + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "organizationUpgraded", + }); + }); + + it("should handle upgrade requiring payment method", async () => { + setupMockUpgradeOrganization(mockOrganizationApiService, organizationsSubject, { + hasPaymentSource: false, + maxStorageGb: 2, + }); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Required for upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("billing form display flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should show appropriate billing fields based on plan type", () => { + // Personal plans (Free, Families) should not require tax ID + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + // Business plans (Teams, Enterprise) should show tax ID field + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + }); + + describe("secrets manager handling flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should prefill SM seats from existing subscription", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + useSecretsManager: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + smSeats: 5, + smServiceAccounts: 75, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams plan + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.changedProduct(); + + expect(upgradeComponent.secretsManagerForm.controls.enabled.value).toBe(true); + expect(upgradeComponent.secretsManagerForm.controls.userSeats.value).toBe(5); + expect(upgradeComponent.secretsManagerForm.controls.additionalServiceAccounts.value).toBe(25); + }); + + it("should enable SM by default when enableSecretsManagerByDefault is true", async () => { + const smFixture = TestBed.createComponent(OrganizationPlansComponent); + const smComponent = smFixture.componentInstance; + smComponent.enableSecretsManagerByDefault = true; + smComponent.productTier = ProductTierType.Teams; + + smFixture.detectChanges(); + await smFixture.whenStable(); + + expect(smComponent.secretsManagerForm.value.enabled).toBe(true); + expect(smComponent.secretsManagerForm.value.userSeats).toBe(1); + expect(smComponent.secretsManagerForm.value.additionalServiceAccounts).toBe(0); + }); + + it("should trigger tax recalculation when SM form changes", fakeAsync(() => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "90210", + }); + + // Clear previous calls + jest.clearAllMocks(); + + // Change SM form + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + }); + + tick(1500); // Wait for debounce + + expect( + mockPreviewInvoiceClient.previewTaxForOrganizationSubscriptionPurchase, + ).toHaveBeenCalled(); + })); + }); + + describe("form update helpers flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should handle premium addon access based on plan features", () => { + // Plan without premium access option should set addon to true (meaning it's included) + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(true); + + // Plan with premium access option should set addon to false (user can opt-in) + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.premiumAccessAddon.value).toBe(false); + }); + + it("should handle additional storage for upgrade with existing data", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + maxStorageGb: 5, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan with 0 GB base + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + expect(upgradeComponent.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should reset additional storage when plan doesn't support it", () => { + component.formGroup.controls.additionalStorage.setValue(10); + component.productTier = ProductTierType.Free; + component.changedProduct(); + + expect(component.formGroup.controls.additionalStorage.value).toBe(0); + }); + + it("should handle additional seats for various scenarios", () => { + // Plan without additional seats option should reset to 0 + component.formGroup.controls.additionalSeats.setValue(10); + component.productTier = ProductTierType.Families; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBe(0); + + // Default to 1 seat for new org with seats option + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + expect(component.formGroup.controls.additionalSeats.value).toBeGreaterThanOrEqual(1); + }); + + it("should prefill seats from current plan when upgrading from non-seats plan", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 2, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free plan (no additional seats) + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.changedProduct(); + + // Should use base seats from current plan + expect(upgradeComponent.formGroup.controls.additionalSeats.value).toBe(2); + }); + }); + + describe("provider creation flow", () => { + beforeEach(() => { + component.providerId = "provider-123"; + mockProviderApiService.getProvider.mockResolvedValue({ + id: "provider-123", + name: "Test Provider", + } as any); + }); + + it("should create organization through provider with wrapped key", async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + patchOrganizationForm(component, { + name: "Provider Client Org", + billingEmail: "client@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + component.formGroup.patchValue({ + clientOwnerEmail: "owner@client.com", + }); + + patchBillingAddress(component); + + const mockOrgKey = {} as any; + const mockProviderKey = {} as any; + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + mockOrgKey, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockKeyService.providerKeys$.mockReturnValue(of({ "provider-123": mockProviderKey })); + + mockEncryptService.wrapSymmetricKey.mockResolvedValue({ + encryptedString: "wrapped-key", + } as any); + + mockApiService.postProviderCreateOrganization.mockResolvedValue({ + organizationId: "provider-org-id", + } as any); + + setupMockPaymentMethodComponent(component); + + await component.submit(); + + expect(mockApiService.postProviderCreateOrganization).toHaveBeenCalledWith( + "provider-123", + expect.objectContaining({ + clientOwnerEmail: "owner@client.com", + }), + ); + + expect(mockEncryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); + }); + }); + + describe("upgrade with missing keys flow", () => { + beforeEach(async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: false, // Missing keys + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + component.organizationId = "org-123"; + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should backfill organization keys during upgrade", async () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + component.formGroup.controls.additionalSeats.setValue(5); + + const mockOrgShareKey = {} as any; + mockKeyService.orgKeys$.mockReturnValue(of({ "org-123": mockOrgShareKey })); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await component.submit(); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalledWith( + "org-123", + expect.objectContaining({ + keys: expect.any(Object), + }), + ); + }); + }); + + describe("trial flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should emit onTrialBillingSuccess when in trial flow", async () => { + component["isInTrialFlow"] = true; + const trialSpy = jest.spyOn(component.onTrialBillingSuccess, "emit"); + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Enterprise, + plan: PlanType.EnterpriseAnnually, + additionalSeats: 10, + }); + + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + component["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "mock_token", + type: "card", + }), + } as any; + + await component.submit(); + + expect(trialSpy).toHaveBeenCalledWith({ + orgId: "trial-org-id", + subLabelText: expect.stringContaining("annual"), + }); + }); + + it("should not navigate away when in trial flow", async () => { + component["isInTrialFlow"] = true; + + component.formGroup.patchValue({ + name: "Trial Org", + billingEmail: "trial@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "trial-org-id", + } as any); + + await component.submit(); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe("upgrade prefill flow", () => { + it("should prefill Families plan for Free tier upgrade", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[0]; // Free + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.FamiliesAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Families); + }); + + it("should prefill Teams plan for Families tier upgrade when TeamsStarter unavailable", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Families, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.FamiliesAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[1]; // Families + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.TeamsAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Teams); + }); + + it("should use upgradeSortOrder for sequential plan upgrades", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.plan).toBe(PlanType.EnterpriseAnnually); + expect(upgradeComponent.productTier).toBe(ProductTierType.Enterprise); + }); + + it("should not prefill for Enterprise tier (no upgrade available)", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Enterprise, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.EnterpriseAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[3]; // Enterprise + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + // Should not change from default Free + expect(upgradeComponent.productTier).toBe(ProductTierType.Free); + }); + }); + + describe("plan filtering logic", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should check if provider is qualified for 2020 plans", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-01-01", // Before cutoff + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(true); + }); + + it("should not qualify provider created after 2020 plan cutoff", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: "2023-12-01", // After cutoff (2023-11-06) + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should return false if provider has no creation date", () => { + component.providerId = "provider-123"; + component["provider"] = { + id: "provider-123", + creationDate: null, + } as any; + + const isQualified = component["isProviderQualifiedFor2020Plan"](); + + expect(isQualified).toBe(false); + }); + + it("should exclude upgrade-ineligible plans", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Teams, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: { type: "card" }, + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.TeamsAnnually, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.currentPlan = mockPasswordManagerPlans[2]; // Teams + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + const products = upgradeComponent.selectableProducts; + + // Should not include plans with lower or equal upgradeSortOrder + expect(products.find((p) => p.type === PlanType.Free)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.FamiliesAnnually)).toBeUndefined(); + expect(products.find((p) => p.type === PlanType.TeamsAnnually)).toBeUndefined(); + }); + }); + + describe("helper calculation methods", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should calculate monthly seat price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // Teams Annual - 48/year + const monthlyPrice = component.seatPriceMonthly(annualPlan); + + expect(monthlyPrice).toBe(4); // 48 / 12 + }); + + it("should calculate monthly storage price correctly", () => { + const annualPlan = mockPasswordManagerPlans[2]; // 4/GB/year + const monthlyPrice = component.additionalStoragePriceMonthly(annualPlan); + + expect(monthlyPrice).toBeCloseTo(0.333, 2); // 4 / 12 + }); + + it("should generate billing sublabel text for annual plan", () => { + component.productTier = ProductTierType.Teams; + component.plan = PlanType.TeamsAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$48"); // Seat price + expect(sublabel).toContain("yr"); + }); + + it("should generate billing sublabel text for plan with base price", () => { + component.productTier = ProductTierType.Families; + component.plan = PlanType.FamiliesAnnually; + + const sublabel = component["billingSubLabelText"](); + + expect(sublabel).toContain("annual"); + expect(sublabel).toContain("$40"); // Base price + }); + }); + + describe("template rendering and UI visibility", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should control form visibility based on loading state", () => { + // Initially not loading after setup + expect(component.loading).toBe(false); + + // When loading + component.loading = true; + expect(component.loading).toBe(true); + + // When not loading + component.loading = false; + expect(component.loading).toBe(false); + }); + + it("should determine createOrganization based on organizationId", () => { + // Create flow - no organizationId + expect(component.createOrganization).toBe(true); + + // Upgrade flow - has organizationId + component.organizationId = "org-123"; + expect(component.createOrganization).toBe(false); + }); + + it("should calculate passwordManagerSubtotal correctly for paid plans", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + + const subtotal = component.passwordManagerSubtotal; + + expect(typeof subtotal).toBe("number"); + expect(subtotal).toBeGreaterThan(0); + }); + + it("should show payment description based on plan type", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + const paymentDesc = component.paymentDesc; + + expect(typeof paymentDesc).toBe("string"); + expect(paymentDesc.length).toBeGreaterThan(0); + }); + + it("should display tax ID field for business plans", () => { + component.productTier = ProductTierType.Free; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Families; + expect(component["showTaxIdField"]).toBe(false); + + component.productTier = ProductTierType.Teams; + expect(component["showTaxIdField"]).toBe(true); + + component.productTier = ProductTierType.Enterprise; + expect(component["showTaxIdField"]).toBe(true); + }); + + it("should show single org policy block when applicable", () => { + component.singleOrgPolicyAppliesToActiveUser = false; + expect(component.singleOrgPolicyBlock).toBe(false); + + component.singleOrgPolicyAppliesToActiveUser = true; + expect(component.singleOrgPolicyBlock).toBe(true); + + // But not when has provider + component.providerId = "provider-123"; + expect(component.singleOrgPolicyBlock).toBe(false); + }); + + it("should determine upgrade requires payment method correctly", async () => { + // Create flow - no organization + expect(component.upgradeRequiresPaymentMethod).toBe(false); + + // Create new component with organization setup + const mockOrg = setupMockUpgradeOrganization( + mockOrganizationApiService, + organizationsSubject, + { + productTierType: ProductTierType.Free, + hasPaymentSource: false, + }, + ); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = mockOrg.id; + upgradeComponent.showFree = false; + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + expect(upgradeComponent.upgradeRequiresPaymentMethod).toBe(true); + }); + }); + + describe("user interactions and form controls", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update component state when product tier changes", () => { + component.productTier = ProductTierType.Free; + + // Simulate changing product tier + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + + expect(component.productTier).toBe(ProductTierType.Teams); + expect(component.formGroup.controls.plan.value).toBe(PlanType.TeamsAnnually); + }); + + it("should update plan when changedOwnedBusiness is called", () => { + component.formGroup.controls.businessOwned.setValue(false); + component.productTier = ProductTierType.Families; + + component.formGroup.controls.businessOwned.setValue(true); + component.changedOwnedBusiness(); + + // Should switch to a business-compatible plan + expect(component.formGroup.controls.productTier.value).toBe(ProductTierType.Teams); + }); + + it("should emit onCanceled when cancel is called", () => { + const cancelSpy = jest.spyOn(component.onCanceled, "emit"); + + component["cancel"](); + + expect(cancelSpy).toHaveBeenCalled(); + }); + + it("should update form value when additional seats changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalSeats.setValue(10); + + expect(component.formGroup.controls.additionalSeats.value).toBe(10); + }); + + it("should update form value when additional storage changes", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.formGroup.controls.additionalStorage.setValue(5); + + expect(component.formGroup.controls.additionalStorage.value).toBe(5); + }); + + it("should mark form as invalid when required fields are empty", () => { + component.formGroup.controls.name.setValue(""); + component.formGroup.controls.billingEmail.setValue(""); + component.formGroup.markAllAsTouched(); + + expect(component.formGroup.invalid).toBe(true); + }); + + it("should mark form as valid when all required fields are filled correctly", () => { + patchOrganizationForm(component, { + name: "Valid Org", + billingEmail: "valid@example.com", + }); + + expect(component.formGroup.valid).toBe(true); + }); + + it("should calculate subtotals based on form values", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + component.formGroup.controls.additionalSeats.setValue(5); + component.formGroup.controls.additionalStorage.setValue(10); + + const subtotal = component.passwordManagerSubtotal; + + // Should include cost of seats and storage + expect(subtotal).toBeGreaterThan(0); + }); + + it("should enable Secrets Manager form when plan supports it", () => { + // Free plan doesn't offer Secrets Manager + component.productTier = ProductTierType.Free; + component.formGroup.controls.productTier.setValue(ProductTierType.Free); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(false); + + // Teams plan offers Secrets Manager + component.productTier = ProductTierType.Teams; + component.formGroup.controls.productTier.setValue(ProductTierType.Teams); + component.changedProduct(); + expect(component.planOffersSecretsManager).toBe(true); + expect(component.secretsManagerForm.disabled).toBe(false); + }); + + it("should update Secrets Manager subtotal when values change", () => { + component.productTier = ProductTierType.Teams; + component.changedProduct(); + + component.secretsManagerForm.patchValue({ + enabled: false, + }); + expect(component.secretsManagerSubtotal).toBe(0); + + component.secretsManagerForm.patchValue({ + enabled: true, + userSeats: 3, + additionalServiceAccounts: 10, + }); + + const smSubtotal = component.secretsManagerSubtotal; + expect(smSubtotal).toBeGreaterThan(0); + }); + }); + + describe("payment method and billing flow", () => { + beforeEach(async () => { + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it("should update payment method during upgrade when required", async () => { + mockOrganization = { + id: "org-123", + name: "Test Org", + productTierType: ProductTierType.Free, + seats: 5, + hasPublicAndPrivateKeys: true, + } as Organization; + + organizationsSubject.next([mockOrganization]); + + mockOrganizationApiService.getBilling.mockResolvedValue({ + paymentSource: null, // No existing payment source + } as any); + + mockOrganizationApiService.getSubscription.mockResolvedValue({ + planType: PlanType.Free, + } as any); + + const upgradeFixture = TestBed.createComponent(OrganizationPlansComponent); + const upgradeComponent = upgradeFixture.componentInstance; + upgradeComponent.organizationId = "org-123"; + upgradeComponent.showFree = false; // Triggers upgradeRequiresPaymentMethod + + upgradeFixture.detectChanges(); + await upgradeFixture.whenStable(); + + upgradeComponent.productTier = ProductTierType.Teams; + upgradeComponent.plan = PlanType.TeamsAnnually; + upgradeComponent.formGroup.controls.additionalSeats.setValue(5); + + upgradeComponent.billingFormGroup.controls.billingAddress.patchValue({ + country: "US", + postalCode: "12345", + line1: "123 Street", + city: "City", + state: "CA", + }); + + upgradeComponent["enterPaymentMethodComponent"] = { + tokenize: jest.fn().mockResolvedValue({ + token: "new_token", + type: "card", + }), + } as any; + + mockOrganizationApiService.upgrade.mockResolvedValue(undefined); + + await upgradeComponent.submit(); + + expect(mockSubscriberBillingClient.updatePaymentMethod).toHaveBeenCalledWith( + { type: "organization", data: mockOrganization }, + { token: "new_token", type: "card" }, + { country: "US", postalCode: "12345" }, + ); + + expect(mockOrganizationApiService.upgrade).toHaveBeenCalled(); + }); + + it("should validate billing form for paid plans during creation", async () => { + component.formGroup.patchValue({ + name: "New Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Teams, + plan: PlanType.TeamsAnnually, + additionalSeats: 5, + }); + + // Invalid billing form - explicitly mark as invalid since we removed validators from mock forms + component.billingFormGroup.controls.billingAddress.patchValue({ + country: "", + postalCode: "", + }); + + await component.submit(); + + expect(mockOrganizationApiService.create).not.toHaveBeenCalled(); + expect(component.billingFormGroup.invalid).toBe(true); + }); + + it("should not require billing validation for Free plan", async () => { + component.formGroup.patchValue({ + name: "Free Org", + billingEmail: "test@example.com", + productTier: ProductTierType.Free, + plan: PlanType.Free, + }); + + // Leave billing form empty + component.billingFormGroup.reset(); + + mockKeyService.makeOrgKey.mockResolvedValue([ + { encryptedString: "mock-key" }, + {} as any, + ] as any); + + mockEncryptService.encryptString.mockResolvedValue({ + encryptedString: "mock-collection", + } as any); + + mockKeyService.makeKeyPair.mockResolvedValue([ + "public-key", + { encryptedString: "private-key" }, + ] as any); + + mockOrganizationApiService.create.mockResolvedValue({ + id: "free-org-id", + } as any); + + await component.submit(); + + expect(mockOrganizationApiService.create).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 3364ce2cbea..73fea30fa83 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -113,8 +113,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentPlan: PlanResponse; - selectedFile: File; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @Input() @@ -675,9 +673,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { const collectionCt = collection.encryptedString; const orgKeys = await this.keyService.makeKeyPair(orgKey[1]); - orgId = this.selfHosted - ? await this.createSelfHosted(key, collectionCt, orgKeys) - : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); + orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); this.toastService.showToast({ variant: "success", @@ -953,27 +949,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } } - private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { - if (!this.selectedFile) { - throw new Error(this.i18nService.t("selectFile")); - } - - const fd = new FormData(); - fd.append("license", this.selectedFile); - fd.append("key", key); - fd.append("collectionName", collectionCt); - const response = await this.organizationApiService.createLicense(fd); - const orgId = response.id; - - await this.apiService.refreshIdentityToken(); - - // Org Keys live outside of the OrganizationLicense - add the keys to the org here - const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); - await this.organizationApiService.updateKeys(orgId, request); - - return orgId; - } - private billingSubLabelText(): string { const selectedPlan = this.selectedPlan; const price = diff --git a/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts index aa6d15401f4..06a6508c6aa 100644 --- a/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts +++ b/apps/web/src/app/billing/payment/components/display-payment-method-inline.component.ts @@ -8,6 +8,7 @@ import { signal, viewChild, } from "@angular/core"; +import { FormGroup } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService, IconComponent } from "@bitwarden/components"; @@ -90,26 +91,28 @@ import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; } @else { -
- - -
+ @if (showFormButtons()) { +
+ + +
+ } } `, @@ -120,51 +123,93 @@ import { EnterPaymentMethodComponent } from "./enter-payment-method.component"; export class DisplayPaymentMethodInlineComponent { readonly subscriber = input.required(); readonly paymentMethod = input.required(); - readonly updated = output(); - readonly changingStateChanged = output(); + readonly externalFormGroup = input(null); - protected formGroup = EnterPaymentMethodComponent.getFormGroup(); + readonly updated = output(); + + protected formGroup: FormGroup; private readonly enterPaymentMethodComponent = viewChild( EnterPaymentMethodComponent, ); - protected readonly isChangingPayment = signal(false); + readonly isChangingPayment = signal(false); + protected readonly cardBrandIcon = computed(() => getCardBrandIcon(this.paymentMethod())); + // Show submit buttons only when component is managing its own form (no external form provided) + protected readonly showFormButtons = computed(() => this.externalFormGroup() === null); + private readonly billingClient = inject(SubscriberBillingClient); private readonly i18nService = inject(I18nService); private readonly toastService = inject(ToastService); private readonly logService = inject(LogService); + constructor() { + // Use external form group if provided, otherwise create our own + this.formGroup = this.externalFormGroup() ?? EnterPaymentMethodComponent.getFormGroup(); + } + /** * Initiates the payment method change process by displaying the inline form. */ protected changePaymentMethod = async (): Promise => { this.isChangingPayment.set(true); - this.changingStateChanged.emit(true); }; + /** + * Public method to get tokenized payment method data. + * Use this when parent component handles submission. + * Parent is responsible for handling billing address separately. + * @returns Promise with tokenized payment method + */ + async getTokenizedPaymentMethod(): Promise { + if (!this.formGroup.valid) { + this.formGroup.markAllAsTouched(); + throw new Error("Form is invalid"); + } + + const component = this.enterPaymentMethodComponent(); + if (!component) { + throw new Error("Payment method component not found"); + } + + const paymentMethod = await component.tokenize(); + if (!paymentMethod) { + throw new Error("Failed to tokenize payment method"); + } + + return paymentMethod; + } + + /** + * Validates the form and returns whether it's ready for submission. + * Used when parent component handles submission to determine button state. + */ + isFormValid(): boolean { + const enterPaymentMethodComponent = this.enterPaymentMethodComponent(); + if (enterPaymentMethodComponent) { + return this.enterPaymentMethodComponent()!.validate(); + } + return false; + } + + /** + * Public method to reset the form and exit edit mode. + * Use this after parent successfully handles the update. + */ + resetForm(): void { + this.formGroup.reset(); + this.isChangingPayment.set(false); + } + /** * Submits the payment method update form. * Validates the form, tokenizes the payment method, and sends the update request. */ protected submit = async (): Promise => { try { - if (!this.formGroup.valid) { - this.formGroup.markAllAsTouched(); - throw new Error("Form is invalid"); - } - - const component = this.enterPaymentMethodComponent(); - if (!component) { - throw new Error("Payment method component not found"); - } - - const paymentMethod = await component.tokenize(); - if (!paymentMethod) { - throw new Error("Failed to tokenize payment method"); - } + const paymentMethod = await this.getTokenizedPaymentMethod(); const billingAddress = this.formGroup.value.type !== TokenizablePaymentMethods.payPal @@ -201,9 +246,7 @@ export class DisplayPaymentMethodInlineComponent { message: this.i18nService.t("paymentMethodUpdated"), }); this.updated.emit(result.value); - this.isChangingPayment.set(false); - this.changingStateChanged.emit(false); - this.formGroup.reset(); + this.resetForm(); break; } case "error": { @@ -223,8 +266,6 @@ export class DisplayPaymentMethodInlineComponent { * Cancels the inline editing and resets the form. */ protected cancel = (): void => { - this.formGroup.reset(); - this.changingStateChanged.emit(false); - this.isChangingPayment.set(false); + this.resetForm(); }; } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index d21b5039d2a..feffc28150e 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -44,16 +44,9 @@ import { InternalUserDecryptionOptionsServiceAbstraction, LoginEmailService, } from "@bitwarden/auth/common"; -import { - AutomaticUserConfirmationService, - DefaultAutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { - InternalOrganizationServiceAbstraction, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService, @@ -67,6 +60,8 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-auth-request-answering.service"; +import { ChangeEmailService } from "@bitwarden/common/auth/services/change-email/change-email.service"; +import { DefaultChangeEmailService } from "@bitwarden/common/auth/services/change-email/default-change-email.service"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; @@ -133,6 +128,7 @@ import { SessionTimeoutSettingsComponentService, } from "@bitwarden/key-management-ui"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; +import { UserCryptoManagementModule } from "@bitwarden/user-crypto-management"; import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service"; import { WebVaultPremiumUpgradePromptService } from "@bitwarden/web-vault/app/vault/services/web-premium-upgrade-prompt.service"; @@ -373,19 +369,6 @@ const safeProviders: SafeProvider[] = [ I18nServiceAbstraction, ], }), - safeProvider({ - provide: AutomaticUserConfirmationService, - useClass: DefaultAutomaticUserConfirmationService, - deps: [ - ConfigService, - ApiService, - OrganizationUserService, - StateProvider, - InternalOrganizationServiceAbstraction, - OrganizationUserApiService, - PolicyService, - ], - }), safeProvider({ provide: SdkLoadService, useClass: flagEnabled("sdk") ? WebSdkLoadService : NoopSdkLoadService, @@ -513,11 +496,22 @@ const safeProviders: SafeProvider[] = [ ConfigService, ], }), + safeProvider({ + provide: ChangeEmailService, + useClass: DefaultChangeEmailService, + deps: [ + ConfigService, + InternalMasterPasswordServiceAbstraction, + KdfConfigService, + ApiService, + KeyServiceAbstraction, + ], + }), ]; @NgModule({ declarations: [], - imports: [CommonModule, JslibServicesModule, GeneratorServicesModule], + imports: [CommonModule, JslibServicesModule, UserCryptoManagementModule, GeneratorServicesModule], // Do not register your dependency here! Add it to the typesafeProviders array using the helper function providers: safeProviders, }) diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 47f4344ec36..006014b9fed 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -355,6 +355,13 @@ export class EventService { this.getShortId(ev.organizationUserId), ); break; + case EventType.OrganizationUser_AutomaticallyConfirmed: + msg = this.i18nService.t("automaticallyConfirmedUserId", this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t( + "automaticallyConfirmedUserId", + this.getShortId(ev.organizationUserId), + ); + break; // Org case EventType.Organization_Updated: msg = humanReadableMsg = this.i18nService.t("editedOrgSettings"); @@ -458,6 +465,18 @@ export class EventService { case EventType.Organization_ItemOrganization_Declined: msg = humanReadableMsg = this.i18nService.t("userDeclinedTransfer"); break; + case EventType.Organization_AutoConfirmEnabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByAdmin"); + break; + case EventType.Organization_AutoConfirmDisabled_Admin: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByAdmin"); + break; + case EventType.Organization_AutoConfirmEnabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByPortal"); + break; + case EventType.Organization_AutoConfirmDisabled_Portal: + msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByPortal"); + break; // Policies case EventType.Policy_Updated: { diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html index 144396d6772..56316fcddee 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.html @@ -43,16 +43,16 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } - + {{ "timesExposed" | i18n }} @@ -60,7 +60,7 @@ - + @if (!organization || canManageCipher(row)) { } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + } + + } @if (cipherDocs.has(row.id)) { diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html index f08af8bda01..66bd11e7bc3 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.html @@ -45,20 +45,20 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - {{ "timesReused" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + {{ "timesReused" | i18n }} + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } {{ "reusedXTimes" | i18n: passwordUseMap.get(row.login.password) }} diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html index 810c1e384b0..553c3f2f04e 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.html @@ -45,19 +45,19 @@ > } } - - @if (!isAdminConsoleActive) { - - - {{ "name" | i18n }} - {{ "owner" | i18n }} - - } + + + + {{ "name" | i18n }} + @if (!isAdminConsoleActive) { + {{ "owner" | i18n }} + } + - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { {{ row.subTitle }} - - @if (!organization) { - - - } - + @if (!isAdminConsoleActive) { + + @if (!organization) { + + + } + + } } diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html index 5a187427b5e..fd5b916e661 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.html @@ -45,12 +45,12 @@ > } } - + - + {{ "name" | i18n }} @if (!isAdminConsoleActive) { - + {{ "owner" | i18n }} } @@ -62,7 +62,7 @@ - + @if (!organization || canManageCipher(row)) { {{ row.name }} } @else { - {{ row.name }} + {{ row.name }} } @if (!organization && row.organizationId) { -
-
- @if (!homepage) { - - {{ "backToReports" | i18n }} - - } +@if (!homepage) { + + +} + + + -
+ diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts index a6d84ccb037..136b70c81e4 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.ts +++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts @@ -1,4 +1,14 @@ -import { Component } from "@angular/core"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { TemplatePortal } from "@angular/cdk/portal"; +import { + AfterViewInit, + Component, + inject, + OnDestroy, + TemplateRef, + viewChild, + ViewContainerRef, +} from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import { filter } from "rxjs/operators"; @@ -10,20 +20,65 @@ import { filter } from "rxjs/operators"; templateUrl: "reports-layout.component.html", standalone: false, }) -export class ReportsLayoutComponent { +export class ReportsLayoutComponent implements AfterViewInit, OnDestroy { homepage = true; - constructor(router: Router) { - const reportsHomeRoute = "/reports"; + private readonly backButtonTemplate = + viewChild.required>("backButtonTemplate"); - this.homepage = router.url === reportsHomeRoute; - router.events + private overlayRef: OverlayRef | null = null; + private overlay = inject(Overlay); + private viewContainerRef = inject(ViewContainerRef); + private router = inject(Router); + + constructor() { + this.router.events .pipe( takeUntilDestroyed(), filter((event) => event instanceof NavigationEnd), ) - .subscribe((event) => { - this.homepage = (event as NavigationEnd).url == reportsHomeRoute; + .subscribe(() => this.updateOverlay()); + } + + ngAfterViewInit(): void { + this.updateOverlay(); + } + + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + returnFocusToPage(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const firstFocusable = document.querySelector( + "[cdktrapfocus] a:not([tabindex='-1'])", + ) as HTMLElement; + firstFocusable?.focus(); + } + + focusOverlayButton(event: Event): void { + if ((event as KeyboardEvent).shiftKey) { + return; // Allow natural Shift+Tab behavior + } + event.preventDefault(); + const button = this.overlayRef?.overlayElement?.querySelector("a") as HTMLElement; + button?.focus(); + } + + private updateOverlay(): void { + if (this.router.url === "/reports") { + this.homepage = true; + this.overlayRef?.dispose(); + this.overlayRef = null; + } else if (!this.overlayRef) { + this.homepage = false; + this.overlayRef = this.overlay.create({ + positionStrategy: this.overlay.position().global().bottom("20px").right("32px"), }); + this.overlayRef.attach(new TemplatePortal(this.backButtonTemplate(), this.viewContainerRef)); + } } } diff --git a/apps/web/src/app/dirt/reports/reports.module.ts b/apps/web/src/app/dirt/reports/reports.module.ts index 4fc152917f4..c4bd9fef809 100644 --- a/apps/web/src/app/dirt/reports/reports.module.ts +++ b/apps/web/src/app/dirt/reports/reports.module.ts @@ -1,3 +1,4 @@ +import { OverlayModule } from "@angular/cdk/overlay"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; @@ -29,6 +30,7 @@ import { ReportsSharedModule } from "./shared"; @NgModule({ imports: [ CommonModule, + OverlayModule, SharedModule, ReportsSharedModule, ReportsRoutingModule, diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html similarity index 100% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.html diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts similarity index 96% rename from apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts rename to apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts index eb84868dca1..ca9042e802e 100644 --- a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts +++ b/apps/web/src/app/key-management/account-fingerprint/account-fingerprint.component.ts @@ -4,7 +4,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { KeyService } from "@bitwarden/key-management"; -import { SharedModule } from "../../shared.module"; +import { SharedModule } from "../../shared/shared.module"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index a2330025c92..fec972c82f2 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -57,6 +57,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { BitwardenClient, PureCrypto } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth"; @@ -287,6 +288,7 @@ describe("KeyRotationService", () => { let mockSdkClientFactory: MockProxy; let mockSecurityStateService: MockProxy; let mockMasterPasswordService: MockProxy; + let mockSdkUserKeyRotationService: MockProxy; const mockUser = { id: "mockUserId" as UserId, @@ -348,6 +350,7 @@ describe("KeyRotationService", () => { mockDialogService = mock(); mockCryptoFunctionService = mock(); mockKdfConfigService = mock(); + mockSdkUserKeyRotationService = mock(); mockSdkClientFactory = mock(); mockSdkClientFactory.createSdkClient.mockResolvedValue({ crypto: () => { @@ -358,6 +361,7 @@ describe("KeyRotationService", () => { } as any; }, } as BitwardenClient); + mockSecurityStateService = mock(); mockMasterPasswordService = mock(); @@ -384,6 +388,7 @@ describe("KeyRotationService", () => { mockSdkClientFactory, mockSecurityStateService, mockMasterPasswordService, + mockSdkUserKeyRotationService, ); }); @@ -509,7 +514,12 @@ describe("KeyRotationService", () => { ); mockKeyService.userSigningKey$.mockReturnValue(new BehaviorSubject(null)); mockSecurityStateService.accountSecurityState$.mockReturnValue(new BehaviorSubject(null)); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockImplementation(async (flag: FeatureFlag) => { + if (flag === FeatureFlag.EnrollAeadOnKeyRotation) { + return true; + } + return false; + }); const spy = jest.spyOn(keyRotationService, "getRotatedAccountKeysFlagged").mockResolvedValue({ userKey: TEST_VECTOR_USER_KEY_V2, diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 68253a4a35d..26dcacd8f11 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -39,6 +39,7 @@ import { KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; import { PureCrypto, TokenProvider } from "@bitwarden/sdk-internal"; +import { UserKeyRotationServiceAbstraction } from "@bitwarden/user-crypto-management"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth/core"; @@ -101,6 +102,7 @@ export class UserKeyRotationService { private sdkClientFactory: SdkClientFactory, private securityStateService: SecurityStateService, private masterPasswordService: MasterPasswordServiceAbstraction, + private sdkUserKeyRotationService: UserKeyRotationServiceAbstraction, ) {} /** @@ -116,6 +118,28 @@ export class UserKeyRotationService { user: Account, newMasterPasswordHint?: string, ): Promise { + const useSdkKeyRotation = await this.configService.getFeatureFlag(FeatureFlag.SdkKeyRotation); + if (useSdkKeyRotation) { + this.logService.info( + "[UserKey Rotation] Using SDK-based key rotation service from user-crypto-management", + ); + await this.sdkUserKeyRotationService.changePasswordAndRotateUserKey( + currentMasterPassword, + newMasterPassword, + newMasterPasswordHint, + asUuid(user.id), + ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("rotationCompletedTitle"), + message: this.i18nService.t("rotationCompletedDesc"), + timeout: 15000, + }); + + await this.logoutService.logout(user.id); + return; + } + // Key-rotation uses the SDK, so we need to ensure that the SDK is loaded / the WASM initialized. await SdkLoadService.Ready; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index 9c8f2125614..9b95737df32 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -1,67 +1,66 @@ -
- @let accessibleProducts = accessibleProducts$ | async; - @if (accessibleProducts && accessibleProducts.length > 1) { - - - - } +@let accessibleProducts = accessibleProducts$ | async; +@if (accessibleProducts && accessibleProducts.length > 1) { + + + +} - @if (shouldShowPremiumUpgradeButton$ | async) { - - } +@if (shouldShowPremiumUpgradeButton$ | async) { + +} - @let moreProducts = moreProducts$ | async; - @if (moreProducts && moreProducts.length > 0) { -
- {{ "moreFromBitwarden" | i18n }} - - + +
+ +
+} diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 9a6de3ad9af..95676759147 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -8,7 +8,7 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { FakeGlobalStateProvider } from "@bitwarden/common/spec"; -import { IconButtonModule, NavigationModule } from "@bitwarden/components"; +import { IconButtonModule, NavigationModule, SideNavService } from "@bitwarden/components"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; @@ -86,6 +86,9 @@ describe("NavigationProductSwitcherComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(NavigationProductSwitcherComponent); + // SideNavService.open starts false (managed by LayoutComponent's ResizeObserver in a real + // app). Set it to true so NavItemComponent renders text labels (used in text-content checks). + TestBed.inject(SideNavService).open.set(true); fixture.detectChanges(); }); diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index ba36063fb7b..990b1f63267 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -1,8 +1,10 @@ import { Component, Directive, importProvidersFrom, Input } from "@angular/core"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { BehaviorSubject, Observable, of } from "rxjs"; +import { PasswordManagerLogo } from "@bitwarden/assets/svg"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; @@ -17,13 +19,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; import { + I18nMockService, LayoutComponent, NavigationModule, StorybookGlobalStateProvider, } from "@bitwarden/components"; -// FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports -import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service"; +import { positionFixedWrapperDecorator } from "@bitwarden/components/src/stories/storybook-decorators"; import { GlobalStateProvider } from "@bitwarden/state"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -105,19 +107,10 @@ class MockBillingAccountProfileStateService implements Partial { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - selector: "story-layout", - template: ``, - standalone: false, -}) -class StoryLayoutComponent {} - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -132,17 +125,23 @@ const translations: Record = { secureYourInfrastructure: "Secure your infrastructure", protectYourFamilyOrBusiness: "Protect your family or business", skipToContent: "Skip to content", + toggleSideNavigation: "Toggle side navigation", + resizeSideNavigation: "Resize side navigation", + submenu: "submenu", + toggleCollapse: "toggle collapse", + close: "Close", + loading: "Loading", }; export default { title: "Web/Navigation Product Switcher", decorators: [ + positionFixedWrapperDecorator(), moduleMetadata({ declarations: [ NavigationProductSwitcherComponent, MockOrganizationService, MockProviderService, - StoryLayoutComponent, StoryContentComponent, ], imports: [NavigationModule, RouterModule, LayoutComponent, I18nPipe], @@ -174,19 +173,11 @@ export default { }), applicationConfig({ providers: [ + provideNoopAnimations(), importProvidersFrom( - RouterModule.forRoot([ - { - path: "", - component: StoryLayoutComponent, - children: [ - { - path: "**", - component: StoryContentComponent, - }, - ], - }, - ]), + RouterModule.forRoot([{ path: "**", component: StoryContentComponent }], { + useHash: true, + }), ), { provide: GlobalStateProvider, @@ -203,12 +194,47 @@ type Story = StoryObj< const Template: Story = { render: (args) => ({ - props: args, + props: { ...args, logo: PasswordManagerLogo }, template: ` - -
- -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, }), }; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index f2154ec74a3..290e07c932a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -19,6 +19,9 @@ " class="tw-group/product-link tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-600 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" ariaCurrentWhenActive="page" + [state]="{ + focusAfterNav: 'body', + }" > { getFeatureFlag$(key: Flag): Observable> { - return of(false); + return of(false as FeatureFlagValueType); } } diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 31fcf9ffe6d..fae8944a10f 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -159,16 +159,14 @@ export class ProductSwitcherService { this.userHasSingleOrgPolicy$, this.route.paramMap, this.triggerProductUpdate$, - this.configService.getFeatureFlag$(FeatureFlag.SM1719_RemoveSecretsManagerAds), ]).pipe( map( - ([orgs, providers, userHasSingleOrgPolicy, paramMap, , removeSecretsManagerAdsFlag]: [ + ([orgs, providers, userHasSingleOrgPolicy, paramMap]: [ Organization[], Provider[], boolean, ParamMap, void, - boolean, ]) => { // Sort orgs by name to match the order within the sidebar orgs.sort((a, b) => a.name.localeCompare(b.name)); @@ -215,13 +213,11 @@ export class ProductSwitcherService { }; // Check if SM ads should be disabled for any organization - // SM ads are only disabled if the feature flag is enabled AND - // the user is a regular User (not Admin or Owner) in an organization that has useDisableSMAdsForUsers enabled - const shouldDisableSMAds = - removeSecretsManagerAdsFlag && - orgs.some( - (org) => org.useDisableSMAdsForUsers === true && org.type === OrganizationUserType.User, - ); + // SM ads are disabled if the user is a regular User (not Admin or Owner) + // in an organization that has useDisableSMAdsForUsers enabled + const shouldDisableSMAds = orgs.some( + (org) => org.useDisableSMAdsForUsers === true && org.type === OrganizationUserType.User, + ); const products = { pm: { diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 10f569e2558..ffc3f57babe 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -3,7 +3,9 @@ - + @if (sendEnabled$ | async) { + + } @@ -18,11 +20,10 @@ } @else { } - + @let subscriptionRoute = subscriptionRoute$ | async; + @if (subscriptionRoute) { + + } @if (showEmergencyAccess()) { ; - protected hasFamilySponsorshipAvailable$: Observable; - protected showSponsoredFamilies$: Observable; - protected showSubscription$: Observable; + protected readonly sendEnabled$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)), + map((isDisabled) => !isDisabled), + ); protected consolidatedSessionTimeoutComponent$: Observable; + protected hasPremiumFromAnyOrganization$: Observable; + protected hasSubscription$: Observable; + protected subscriptionRoute$: Observable; constructor( private syncService: SyncService, @@ -50,13 +59,8 @@ export class UserLayoutComponent implements OnInit { private accountService: AccountService, private policyService: PolicyService, private configService: ConfigService, + private accountBillingClient: AccountBillingClient, ) { - this.showSubscription$ = this.accountService.activeAccount$.pipe( - switchMap((account) => - this.billingAccountProfileStateService.canViewSubscription$(account.id), - ), - ); - this.showEmergencyAccess = toSignal( this.accountService.activeAccount$.pipe( getUserId, @@ -69,10 +73,41 @@ export class UserLayoutComponent implements OnInit { this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$( FeatureFlag.ConsolidatedSessionTimeoutComponent, ); + + this.hasPremiumFromAnyOrganization$ = this.ifAccountExistsCheck((userId) => + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(userId), + ); + + this.hasSubscription$ = this.ifAccountExistsCheck(() => + from(this.accountBillingClient.getSubscription()).pipe( + map((subscription) => !!subscription), + catchError(() => of(false)), + ), + ); + + this.subscriptionRoute$ = combineLatest([ + this.hasSubscription$, + this.hasPremiumFromAnyOrganization$, + ]).pipe( + map(([hasSubscription, hasPremiumFromAnyOrganization]) => { + if (!hasPremiumFromAnyOrganization || hasSubscription) { + return hasSubscription + ? "settings/subscription/user-subscription" + : "settings/subscription/premium"; + } + return null; + }), + ); } async ngOnInit() { document.body.classList.remove("layout_frontend"); await this.syncService.fullSync(false); } + + private ifAccountExistsCheck(predicate$: (userId: UserId) => Observable) { + return this.accountService.activeAccount$.pipe( + switchMap((account) => (account ? predicate$(account.id) : of(false))), + ); + } } diff --git a/apps/web/src/app/layouts/web-side-nav.component.html b/apps/web/src/app/layouts/web-side-nav.component.html index adb526bd593..081afc355a6 100644 --- a/apps/web/src/app/layouts/web-side-nav.component.html +++ b/apps/web/src/app/layouts/web-side-nav.component.html @@ -1,8 +1,12 @@ - + + + + + diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 932d0b8119b..a5fe3f5d627 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; import { organizationPolicyGuard } from "@bitwarden/angular/admin-console/guards"; import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; @@ -50,6 +51,7 @@ import { NewDeviceVerificationComponent, } from "@bitwarden/auth/angular"; import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, RemovePasswordComponent } from "@bitwarden/key-management-ui"; @@ -641,6 +643,13 @@ const routes: Routes = [ path: "sends", component: SendComponent, data: { titleId: "send" } satisfies RouteDataProperties, + canActivate: [ + organizationPolicyGuard((userId, _configService, policyService) => + policyService + .policyAppliesToUser$(PolicyType.DisableSend, userId) + .pipe(map((policyApplies) => !policyApplies)), + ), + ], }, { path: "sm-landing", diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 729238e0b0d..e3da37aafed 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -35,11 +35,10 @@ import { } from "@bitwarden/components"; /** - * This NgModule should contain the most basic shared directives, pipes, and components. They - * should be widely used by other modules to be considered for adding to this module. If in doubt - * do not add to this module. + * @deprecated Please directly import the relevant directive/pipe/component. * - * See: https://angular.io/guide/module-types#shared-ngmodules + * This module is overly large and adds many unrelated modules to your dependency tree. + * https://angular.dev/guide/ngmodules/overview recommends not using `NgModule`s for new code. */ @NgModule({ imports: [ diff --git a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.html b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.html index d1ee9d29ebd..2e681cae12d 100644 --- a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.html +++ b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.html @@ -1,17 +1,19 @@ - - + + diff --git a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.ts b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.ts index 68c8c188d31..b49f615fc74 100644 --- a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.ts +++ b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { firstValueFrom, Observable, of, switchMap, lastValueFrom } from "rxjs"; @@ -9,7 +8,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SendType } from "@bitwarden/common/tools/send/types/send-type"; -import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, IconComponent, MenuModule } from "@bitwarden/components"; import { DefaultSendFormConfigService, SendAddEditDialogComponent, @@ -23,7 +22,7 @@ import { SendSuccessDrawerDialogComponent } from "../shared"; @Component({ selector: "tools-new-send-dropdown", templateUrl: "new-send-dropdown.component.html", - imports: [JslibModule, CommonModule, ButtonModule, MenuModule, PremiumBadgeComponent], + imports: [JslibModule, ButtonModule, MenuModule, PremiumBadgeComponent, IconComponent], providers: [DefaultSendFormConfigService], }) /** diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.html b/apps/web/src/app/tools/send/send-access/send-access-email.component.html index ee5a03670bb..03af33ce911 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.html @@ -20,16 +20,19 @@ {{ "verificationCode" | i18n }} -
- -
+ + } diff --git a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts index b1374cd6c66..0915a47e4ad 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-email.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-email.component.ts @@ -1,6 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + effect, + input, + OnDestroy, + OnInit, + output, +} from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { SharedModule } from "../../../shared"; @@ -18,18 +26,45 @@ export class SendAccessEmailComponent implements OnInit, OnDestroy { protected otp: FormControl; readonly loading = input.required(); + readonly backToEmail = output(); constructor() {} ngOnInit() { this.email = new FormControl("", Validators.required); - this.otp = new FormControl("", Validators.required); + this.otp = new FormControl(""); this.formGroup().addControl("email", this.email); this.formGroup().addControl("otp", this.otp); - } + // Update validators when enterOtp changes + effect(() => { + const isOtpMode = this.enterOtp(); + if (isOtpMode) { + // In OTP mode: email is not required (already entered), otp is required + this.email.clearValidators(); + this.otp.setValidators([Validators.required]); + } else { + // In email mode: email is required, otp is not required + this.email.setValidators([Validators.required]); + this.otp.clearValidators(); + } + this.email.updateValueAndValidity(); + this.otp.updateValueAndValidity(); + }); + } ngOnDestroy() { this.formGroup().removeControl("email"); this.formGroup().removeControl("otp"); } + + onBackClick() { + this.backToEmail.emit(); + if (this.otp) { + this.otp.clearValidators(); + this.otp.setValue(""); + this.otp.setErrors(null); + this.otp.markAsUntouched(); + this.otp.markAsPristine(); + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-access-password.component.html b/apps/web/src/app/tools/send/send-access/send-access-password.component.html index deca7ad3d24..53526154773 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-password.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-password.component.html @@ -1,8 +1,7 @@ -

{{ "sendProtectedPassword" | i18n }}

-

{{ "sendProtectedPasswordDontKnow" | i18n }}

{{ "password" | i18n }} + {{ "sendProtectedPasswordDontKnow" | i18n }}
diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.html b/apps/web/src/app/tools/send/send-access/send-access-text.component.html index ca772251146..746dd5f0567 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.html +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.html @@ -1,26 +1,23 @@ -{{ "sendHiddenByDefault" | i18n }} +
+ @if (send.text.hidden) { + + } +
-
-
-
diff --git a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts index 794cfbc9678..8a947eafb69 100644 --- a/apps/web/src/app/tools/send/send-access/send-access-text.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-access-text.component.ts @@ -6,7 +6,7 @@ import { FormBuilder } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; -import { ToastService } from "@bitwarden/components"; +import { IconModule, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -15,7 +15,7 @@ import { SharedModule } from "../../../shared"; @Component({ selector: "app-send-access-text", templateUrl: "send-access-text.component.html", - imports: [SharedModule], + imports: [SharedModule, IconModule], }) export class SendAccessTextComponent { private _send: SendAccessView = null; diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.html b/apps/web/src/app/tools/send/send-access/send-auth.component.html index c3e90cea4ea..8f9b05d7d13 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.html +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.html @@ -1,13 +1,3 @@ -@if (loading()) { -
- - {{ "loading" | i18n }} -
-} @if (error()) {
@@ -31,6 +21,7 @@ [formGroup]="sendAccessForm" [enterOtp]="enterOtp()" [loading]="loading()" + (backToEmail)="onBackToEmail()" > } } diff --git a/apps/web/src/app/tools/send/send-access/send-auth.component.ts b/apps/web/src/app/tools/send/send-access/send-auth.component.ts index 9ed8106ad40..5582d6d8dcc 100644 --- a/apps/web/src/app/tools/send/send-access/send-auth.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -26,7 +26,7 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { AuthType } from "@bitwarden/common/tools/send/types/auth-type"; -import { ToastService } from "@bitwarden/components"; +import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; @@ -52,6 +52,7 @@ export class SendAuthComponent implements OnInit { authType = AuthType; private expiredAuthAttempts = 0; + private otpSubmitted = false; readonly loading = signal(false); readonly error = signal(false); @@ -69,6 +70,7 @@ export class SendAuthComponent implements OnInit { private formBuilder: FormBuilder, private configService: ConfigService, private sendTokenService: SendTokenService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) {} ngOnInit() { @@ -79,13 +81,22 @@ export class SendAuthComponent implements OnInit { this.loading.set(true); this.unavailable.set(false); this.error.set(false); - const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP); - if (sendEmailOtp) { - await this.attemptV2Access(); - } else { - await this.attemptV1Access(); + try { + const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP); + if (sendEmailOtp) { + await this.attemptV2Access(); + } else { + await this.attemptV1Access(); + } + } finally { + this.loading.set(false); } - this.loading.set(false); + } + + onBackToEmail() { + this.enterOtp.set(false); + this.otpSubmitted = false; + this.updatePageTitle(); } private async attemptV1Access() { @@ -103,7 +114,27 @@ export class SendAuthComponent implements OnInit { } catch (e) { if (e instanceof ErrorResponse) { if (e.statusCode === 401) { + if (this.sendAuthType() === AuthType.Password) { + // Password was already required, so this is an invalid password error + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } + } + // Set auth type to Password (either first time or refresh) this.sendAuthType.set(AuthType.Password); + } else if (e.statusCode === 400 && this.sendAuthType() === AuthType.Password) { + // Server returns 400 for SendAccessResult.PasswordInvalid + const passwordControl = this.sendAccessForm.get("password"); + if (passwordControl) { + passwordControl.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, + }); + passwordControl.markAsTouched(); + } } else if (e.statusCode === 404) { this.unavailable.set(true); } else { @@ -160,22 +191,32 @@ export class SendAuthComponent implements OnInit { this.expiredAuthAttempts = 0; if (emailRequired(response.error)) { this.sendAuthType.set(AuthType.Email); + this.updatePageTitle(); } else if (emailAndOtpRequired(response.error)) { this.enterOtp.set(true); + if (this.otpSubmitted) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), + }); + } + this.otpSubmitted = true; + this.updatePageTitle(); } else if (otpInvalid(response.error)) { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidVerificationCode"), + message: this.i18nService.t("invalidEmailOrVerificationCode"), }); } else if (passwordHashB64Required(response.error)) { this.sendAuthType.set(AuthType.Password); + this.updatePageTitle(); } else if (passwordHashB64Invalid(response.error)) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("invalidSendPassword"), + this.sendAccessForm.controls.password?.setErrors({ + invalidPassword: { message: this.i18nService.t("sendPasswordInvalidAskOwner") }, }); + this.sendAccessForm.controls.password?.markAsTouched(); } else if (sendIdInvalid(response.error)) { this.unavailable.set(true); } else { @@ -207,4 +248,26 @@ export class SendAuthComponent implements OnInit { ); return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64; } + + private updatePageTitle(): void { + const authType = this.sendAuthType(); + + if (authType === AuthType.Email) { + if (this.enterOtp()) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "enterTheCodeSentToYourEmail" }, + pageSubtitle: this.sendAccessForm.value.email ?? null, + }); + } else { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "verifyYourEmailToViewThisSend" }, + pageSubtitle: null, + }); + } + } else if (authType === AuthType.Password) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessPasswordTitle" }, + }); + } + } } diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.html b/apps/web/src/app/tools/send/send-access/send-view.component.html index 3536499ddad..ca75f123c7e 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.html +++ b/apps/web/src/app/tools/send/send-access/send-view.component.html @@ -9,12 +9,7 @@ @if (loading()) {
- - {{ "loading" | i18n }} +
} @else { @if (unavailable()) { @@ -47,7 +42,11 @@ } } @if (expirationDate()) { -

Expires: {{ expirationDate() | date: "medium" }}

+ @let formattedExpirationTime = expirationDate() | date: "shortTime"; + @let formattedExpirationDate = expirationDate() | date: "mediumDate"; +

+ {{ "sendExpiresOn" | i18n: formattedExpirationTime : formattedExpirationDate }} +

}
} diff --git a/apps/web/src/app/tools/send/send-access/send-view.component.ts b/apps/web/src/app/tools/send/send-access/send-view.component.ts index 1ab9a121ace..2d9766ded6c 100644 --- a/apps/web/src/app/tools/send/send-access/send-view.component.ts +++ b/apps/web/src/app/tools/send/send-access/send-view.component.ts @@ -21,7 +21,11 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendType } from "@bitwarden/common/tools/send/types/send-type"; -import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; +import { + AnonLayoutWrapperDataService, + SpinnerComponent, + ToastService, +} from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; @@ -32,7 +36,7 @@ import { SendAccessTextComponent } from "./send-access-text.component"; @Component({ selector: "app-send-view", templateUrl: "send-view.component.html", - imports: [SendAccessFileComponent, SendAccessTextComponent, SharedModule], + imports: [SendAccessFileComponent, SendAccessTextComponent, SharedModule, SpinnerComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class SendViewComponent implements OnInit { @@ -69,6 +73,9 @@ export class SendViewComponent implements OnInit { ) {} ngOnInit() { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "sendAccessContentTitle" }, + }); void this.load(); } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts index 1c0c76f84d9..0293245c733 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.spec.ts @@ -86,7 +86,6 @@ describe("VaultItemDialogComponent", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: DIALOG_DATA, useValue: { ...baseParams } }, { provide: DialogRef, useValue: {} }, - { provide: DialogService, useValue: {} }, { provide: ToastService, useValue: { @@ -173,7 +172,9 @@ describe("VaultItemDialogComponent", () => { { provide: SyncService, useValue: {} }, { provide: CipherRiskService, useValue: {} }, ], - }).compileComponents(); + }) + .overrideProvider(DialogService, { useValue: {} }) + .compileComponents(); fixture = TestBed.createComponent(TestVaultItemDialogComponent); component = fixture.componentInstance; @@ -374,6 +375,29 @@ describe("VaultItemDialogComponent", () => { }); }); + describe("disableEdit", () => { + it("returns false when formConfig mode is partial-edit even if canEdit is false", () => { + component["canEdit"] = false; + component.setTestFormConfig({ ...baseFormConfig, mode: "partial-edit" }); + + expect(component["disableEdit"]).toBe(false); + }); + + it("returns true when canEdit is false and formConfig mode is not partial-edit", () => { + component["canEdit"] = false; + component.setTestFormConfig({ ...baseFormConfig, mode: "edit" }); + + expect(component["disableEdit"]).toBe(true); + }); + + it("returns false when canEdit is true regardless of formConfig mode", () => { + component["canEdit"] = true; + component.setTestFormConfig({ ...baseFormConfig, mode: "edit" }); + + expect(component["disableEdit"]).toBe(false); + }); + }); + describe("changeMode", () => { beforeEach(() => { component.setTestCipher({ type: CipherType.Login, id: "cipher-id" }); diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 2340f74c32d..bff760c5178 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -100,7 +100,7 @@ export interface VaultItemDialogParams { /** * Function to restore a cipher from the trash. */ - restore?: (c: CipherViewLike) => Promise; + restore?: (c: CipherViewLike) => Promise; } export const VaultItemDialogResult = { @@ -273,7 +273,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { } protected get disableEdit() { - return !this.canEdit; + return !this.canEdit && this.formConfig.mode !== "partial-edit"; } protected get showEdit() { @@ -396,7 +396,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ); // If user cannot edit and dialog opened in form mode, force to view mode - if (!this.canEdit && this.params.mode === "form") { + if (!this.canEdit && this.formConfig.mode !== "partial-edit" && this.params.mode === "form") { this.params.mode = "view"; this.loadForm = false; this.updateTitle(); diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 3e62ccfd21d..942cb1cdf2e 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -15,6 +15,7 @@
+ diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.spec.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.spec.ts index 49c9df8d582..26c6be65fca 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.spec.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.spec.ts @@ -16,9 +16,11 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { IconButtonModule, MenuModule } from "@bitwarden/components"; -import { CopyCipherFieldDirective, CopyCipherFieldService } from "@bitwarden/vault"; - -import { OrganizationNameBadgeComponent } from "../../individual-vault/organization-badge/organization-name-badge.component"; +import { + CopyCipherFieldDirective, + CopyCipherFieldService, + OrganizationNameBadgeComponent, +} from "@bitwarden/vault"; import { VaultCipherRowComponent } from "./vault-cipher-row.component"; @@ -45,7 +47,7 @@ describe("VaultCipherRowComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [VaultCipherRowComponent, OrganizationNameBadgeComponent], + declarations: [VaultCipherRowComponent], imports: [ CommonModule, RouterModule.forRoot([]), @@ -53,6 +55,7 @@ describe("VaultCipherRowComponent", () => { IconButtonModule, JslibModule, CopyCipherFieldDirective, + OrganizationNameBadgeComponent, ], providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, diff --git a/apps/web/src/app/vault/components/vault-items/vault-item-event.ts b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts index 8cd4b98af40..055592da7d1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-item-event.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts @@ -1,11 +1,12 @@ import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { VaultItemEvent as BaseVaultItemEvent } from "@bitwarden/vault"; import { CollectionPermission } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector"; -import { VaultItem } from "./vault-item"; - +// Extend base events with web-specific events export type VaultItemEvent = - | { type: "viewAttachments"; item: C } + | BaseVaultItemEvent + | { type: "copyField"; item: C; field: "username" | "password" | "totp" } | { type: "bulkEditCollectionAccess"; items: CollectionView[] } | { type: "viewCollectionAccess"; @@ -13,15 +14,4 @@ export type VaultItemEvent = readonly: boolean; initialPermission?: CollectionPermission; } - | { type: "viewEvents"; item: C } - | { type: "editCollection"; item: CollectionView; readonly: boolean } - | { type: "clone"; item: C } - | { type: "restore"; items: C[] } - | { type: "delete"; items: VaultItem[] } - | { type: "copyField"; item: C; field: "username" | "password" | "totp" } - | { type: "moveToFolder"; items: C[] } - | { type: "assignToCollections"; items: C[] } - | { type: "archive"; items: C[] } - | { type: "unarchive"; items: C[] } - | { type: "toggleFavorite"; item: C } - | { type: "editCipher"; item: C }; + | { type: "editCollection"; item: CollectionView; readonly: boolean }; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts index ac74e75f07c..cdc599a6c3e 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts @@ -11,9 +11,8 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { MenuModule, TableModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { RoutedVaultFilterService, RoutedVaultFilterModel } from "@bitwarden/vault"; +import { RoutedVaultFilterService, RoutedVaultFilterModel, VaultItem } from "@bitwarden/vault"; -import { VaultItem } from "./vault-item"; import { VaultItemsComponent } from "./vault-items.component"; describe("VaultItemsComponent", () => { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 96c23d6dd19..e89aee85ef6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -31,7 +31,7 @@ import { } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { SortDirection, TableDataSource } from "@bitwarden/components"; import { OrganizationId } from "@bitwarden/sdk-internal"; -import { RoutedVaultFilterService } from "@bitwarden/vault"; +import { RoutedVaultFilterService, VaultItem } from "@bitwarden/vault"; import { GroupView } from "../../../admin-console/organizations/core"; @@ -39,7 +39,6 @@ import { CollectionPermission, convertToPermission, } from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; -import { VaultItem } from "./vault-item"; import { VaultItemEvent } from "./vault-item-event"; // Fixed manual row height required due to how cdk-virtual-scroll works diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html new file mode 100644 index 00000000000..3304fa3e3cc --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.html @@ -0,0 +1,27 @@ + +
+ +
+
+

+ {{ "vaultWelcomeDialogTitle" | i18n }} +

+

+ {{ "vaultWelcomeDialogDescription" | i18n }} +

+
+
+
+
+ + +
+
diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts new file mode 100644 index 00000000000..bc0142b374d --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { + VaultWelcomeDialogComponent, + VaultWelcomeDialogResult, +} from "./vault-welcome-dialog.component"; + +describe("VaultWelcomeDialogComponent", () => { + let component: VaultWelcomeDialogComponent; + let fixture: ComponentFixture; + + const mockUserId = "user-123" as UserId; + const activeAccount$ = new BehaviorSubject({ + id: mockUserId, + } as Account); + const setUserState = jest.fn().mockResolvedValue([mockUserId, true]); + const close = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await TestBed.configureTestingModule({ + imports: [VaultWelcomeDialogComponent], + providers: [ + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: StateProvider, useValue: { setUserState } }, + { provide: DialogRef, useValue: { close } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultWelcomeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("onDismiss", () => { + it("should set acknowledged state and close with Dismissed result", async () => { + await component["onDismiss"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.Dismissed); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onDismiss"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); + + describe("onPrimaryCta", () => { + it("should set acknowledged state and close with GetStarted result", async () => { + activeAccount$.next({ id: mockUserId } as Account); + + await component["onPrimaryCta"](); + + expect(setUserState).toHaveBeenCalledWith( + expect.objectContaining({ key: "vaultWelcomeDialogAcknowledged" }), + true, + mockUserId, + ); + expect(close).toHaveBeenCalledWith(VaultWelcomeDialogResult.GetStarted); + }); + + it("should throw if no active account", async () => { + activeAccount$.next(null); + + await expect(component["onPrimaryCta"]()).rejects.toThrow("Null or undefined account"); + + expect(setUserState).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts new file mode 100644 index 00000000000..d43ea5165f7 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-welcome-dialog/vault-welcome-dialog.component.ts @@ -0,0 +1,69 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, inject } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + TypographyModule, + CenterPositionStrategy, +} from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +export const VaultWelcomeDialogResult = { + Dismissed: "dismissed", + GetStarted: "getStarted", +} as const; + +export type VaultWelcomeDialogResult = + (typeof VaultWelcomeDialogResult)[keyof typeof VaultWelcomeDialogResult]; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +@Component({ + selector: "app-vault-welcome-dialog", + templateUrl: "./vault-welcome-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, JslibModule], +}) +export class VaultWelcomeDialogComponent { + private accountService = inject(AccountService); + private stateProvider = inject(StateProvider); + + constructor(private dialogRef: DialogRef) {} + + protected async onDismiss(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.Dismissed); + } + + protected async onPrimaryCta(): Promise { + await this.setAcknowledged(); + this.dialogRef.close(VaultWelcomeDialogResult.GetStarted); + } + + private async setAcknowledged(): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.stateProvider.setUserState(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, true, userId); + } + + static open(dialogService: DialogService): DialogRef { + return dialogService.open(VaultWelcomeDialogComponent, { + disableClose: true, + positionStrategy: new CenterPositionStrategy(), + }); + } +} diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html new file mode 100644 index 00000000000..e9932ac9022 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.html @@ -0,0 +1,34 @@ +
+ +
+

+ {{ "extensionPromptHeading" | i18n }} +

+

+ {{ "extensionPromptBody" | i18n }} +

+
+ + + + {{ "downloadExtension" | i18n }} + + +
+
+
diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts new file mode 100644 index 00000000000..fdf218d8c35 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.spec.ts @@ -0,0 +1,86 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +import { WebVaultExtensionPromptDialogComponent } from "./web-vault-extension-prompt-dialog.component"; + +describe("WebVaultExtensionPromptDialogComponent", () => { + let component: WebVaultExtensionPromptDialogComponent; + let fixture: ComponentFixture; + let mockDialogRef: MockProxy>; + + const mockUserId = "test-user-id" as UserId; + + const getDevice = jest.fn(() => DeviceType.ChromeBrowser); + const mockUpdate = jest.fn().mockResolvedValue(undefined); + + const getDialogDismissedState = jest.fn().mockReturnValue({ + update: mockUpdate, + }); + + beforeEach(async () => { + const mockAccountService = mockAccountServiceWith(mockUserId); + mockDialogRef = mock>(); + + await TestBed.configureTestingModule({ + imports: [WebVaultExtensionPromptDialogComponent], + providers: [ + provideNoopAnimations(), + { + provide: PlatformUtilsService, + useValue: { getDevice }, + }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: AccountService, useValue: mockAccountService }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: DialogService, useValue: mock() }, + { + provide: WebVaultExtensionPromptService, + useValue: { getDialogDismissedState }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(WebVaultExtensionPromptDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("ngOnInit", () => { + it("sets webStoreUrl", () => { + expect(getDevice).toHaveBeenCalled(); + + expect(component["webStoreUrl"]).toBe( + "https://chromewebstore.google.com/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb", + ); + }); + }); + + describe("dismissPrompt", () => { + it("calls webVaultExtensionPromptService.getDialogDismissedState and updates to true", async () => { + await component.dismissPrompt(); + + expect(getDialogDismissedState).toHaveBeenCalledWith(mockUserId); + expect(mockUpdate).toHaveBeenCalledWith(expect.any(Function)); + + const updateFn = mockUpdate.mock.calls[0][0]; + expect(updateFn()).toBe(true); + }); + + it("closes the dialog", async () => { + await component.dismissPrompt(); + + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts new file mode 100644 index 00000000000..e5dcf5e3cf2 --- /dev/null +++ b/apps/web/src/app/vault/components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component.ts @@ -0,0 +1,51 @@ +import { CommonModule } from "@angular/common"; +import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; +import { + ButtonModule, + DialogModule, + DialogRef, + DialogService, + IconComponent, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { WebVaultExtensionPromptService } from "../../services/web-vault-extension-prompt.service"; + +@Component({ + selector: "web-vault-extension-prompt-dialog", + templateUrl: "./web-vault-extension-prompt-dialog.component.html", + imports: [CommonModule, ButtonModule, DialogModule, I18nPipe, IconComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WebVaultExtensionPromptDialogComponent implements OnInit { + constructor( + private platformUtilsService: PlatformUtilsService, + private accountService: AccountService, + private dialogRef: DialogRef, + private webVaultExtensionPromptService: WebVaultExtensionPromptService, + ) {} + + /** Download Url for the extension based on the browser */ + protected webStoreUrl: string = ""; + + ngOnInit(): void { + this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice()); + } + + async dismissPrompt() { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.webVaultExtensionPromptService.getDialogDismissedState(userId).update(() => true); + this.dialogRef.close(); + } + + /** Opens the web extension prompt generator dialog. */ + static open(dialogService: DialogService) { + return dialogService.open(WebVaultExtensionPromptDialogComponent); + } +} diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts index 3eecd4ee549..1f3f285d153 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.spec.ts @@ -87,7 +87,6 @@ describe("AddEditComponentV2", () => { { provide: DIALOG_DATA, useValue: mockParams }, { provide: DialogRef, useValue: dialogRef }, { provide: I18nService, useValue: { t: jest.fn().mockReturnValue("login") } }, - { provide: DialogService, useValue: dialogService }, { provide: CipherService, useValue: cipherService }, { provide: MessagingService, useValue: messagingService }, { provide: OrganizationService, useValue: organizationService }, @@ -105,7 +104,9 @@ describe("AddEditComponentV2", () => { }, { provide: AccountService, useValue: accountService }, ], - }).compileComponents(); + }) + .overrideProvider(DialogService, { useValue: dialogService }) + .compileComponents(); fixture = TestBed.createComponent(AddEditComponentV2); component = fixture.componentInstance; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts index e585d165d42..71d14517c8e 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts @@ -1,12 +1,14 @@ import { NgModule } from "@angular/core"; +import { OrganizationNameBadgeComponent } from "@bitwarden/vault"; + import { SharedModule } from "../../../shared/shared.module"; -import { OrganizationNameBadgeComponent } from "./organization-name-badge.component"; - +/** + * @deprecated Use `OrganizationNameBadgeComponent` directly since it is now standalone. + */ @NgModule({ - imports: [SharedModule], - declarations: [OrganizationNameBadgeComponent], + imports: [SharedModule, OrganizationNameBadgeComponent], exports: [OrganizationNameBadgeComponent], }) export class OrganizationBadgeModule {} diff --git a/apps/web/src/app/vault/individual-vault/pipes/pipes.module.ts b/apps/web/src/app/vault/individual-vault/pipes/pipes.module.ts index 16b95717718..acfc4c967e5 100644 --- a/apps/web/src/app/vault/individual-vault/pipes/pipes.module.ts +++ b/apps/web/src/app/vault/individual-vault/pipes/pipes.module.ts @@ -1,10 +1,12 @@ import { NgModule } from "@angular/core"; +import { GetOrgNameFromIdPipe } from "@bitwarden/vault"; + import { GetGroupNameFromIdPipe } from "./get-group-name.pipe"; -import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe"; @NgModule({ - declarations: [GetOrgNameFromIdPipe, GetGroupNameFromIdPipe], + imports: [GetOrgNameFromIdPipe], + declarations: [GetGroupNameFromIdPipe], exports: [GetOrgNameFromIdPipe, GetGroupNameFromIdPipe], }) export class PipesModule {} diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6fbe3f08912..5045afd3cbe 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,15 +1,6 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; -import { ActivatedRoute, Params, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - firstValueFrom, - lastValueFrom, - Observable, - Subject, -} from "rxjs"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, viewChild } from "@angular/core"; +import { ActivatedRoute, NavigationExtras, Params, Router } from "@angular/router"; +import { combineLatest, firstValueFrom, lastValueFrom, Observable, of, Subject } from "rxjs"; import { concatMap, debounceTime, @@ -18,6 +9,7 @@ import { first, map, shareReplay, + startWith, switchMap, take, takeUntil, @@ -89,7 +81,6 @@ import { CipherListView } from "@bitwarden/sdk-internal"; import { AddEditFolderDialogComponent, AddEditFolderDialogResult, - AttachmentDialogCloseResult, AttachmentDialogResult, AttachmentsV2Component, CipherFormConfig, @@ -108,6 +99,7 @@ import { OrganizationFilter, VaultItemsTransferService, DefaultVaultItemsTransferService, + VaultItem, } from "@bitwarden/vault"; import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; @@ -124,7 +116,6 @@ import { VaultItemDialogMode, VaultItemDialogResult, } from "../components/vault-item-dialog/vault-item-dialog.component"; -import { VaultItem } from "../components/vault-items/vault-item"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsComponent } from "../components/vault-items/vault-items.component"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; @@ -179,14 +170,10 @@ type EmptyStateMap = Record; ], }) export class VaultComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + readonly filterComponent = viewChild(VaultFilterComponent); + readonly vaultItemsComponent = viewChild(VaultItemsComponent); - trashCleanupWarning: string = null; + trashCleanupWarning: string = ""; activeFilter: VaultFilter = new VaultFilter(); protected deactivatedOrgIcon = DeactivatedOrg; @@ -198,20 +185,20 @@ export class VaultComponent implements OnInit, OnDestr protected refreshing = false; protected processingEvent = false; protected filter: RoutedVaultFilterModel = {}; - protected showBulkMove: boolean; - protected canAccessPremium: boolean; - protected allCollections: CollectionView[]; + protected showBulkMove: boolean = false; + protected canAccessPremium: boolean = false; + protected allCollections: CollectionView[] = []; protected allOrganizations: Organization[] = []; - protected ciphers: C[]; - protected collections: CollectionView[]; - protected isEmpty: boolean; + protected ciphers: C[] = []; + protected collections: CollectionView[] = []; + protected isEmpty: boolean = false; protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable = this.route.queryParams.pipe( map((queryParams) => queryParams.search), ); private searchText$ = new Subject(); - private refresh$ = new BehaviorSubject(null); + private refresh$ = new Subject(); private destroy$ = new Subject(); private vaultItemDialogRef?: DialogRef | undefined; @@ -220,7 +207,7 @@ export class VaultComponent implements OnInit, OnDestr organizations$ = this.accountService.activeAccount$ .pipe(map((a) => a?.id)) - .pipe(switchMap((id) => this.organizationService.organizations$(id))); + .pipe(switchMap((id) => (id ? this.organizationService.organizations$(id) : of([])))); emptyState$ = combineLatest([ this.currentSearchText$, @@ -228,7 +215,7 @@ export class VaultComponent implements OnInit, OnDestr this.organizations$, ]).pipe( map(([searchText, filter, organizations]) => { - const selectedOrg = organizations?.find((org) => org.id === filter.organizationId); + const selectedOrg = organizations.find((org) => org.id === filter.organizationId); const isOrgDisabled = selectedOrg && !selectedOrg.enabled; if (isOrgDisabled) { @@ -411,7 +398,7 @@ export class VaultComponent implements OnInit, OnDestr queryParamsHandling: "merge", replaceUrl: true, state: { - focusMainAfterNav: false, + focusAfterNav: false, }, }), ); @@ -586,7 +573,7 @@ export class VaultComponent implements OnInit, OnDestr firstSetup$ .pipe( - switchMap(() => this.refresh$), + switchMap(() => this.refresh$.pipe(startWith(undefined))), tap(() => (this.refreshing = true)), switchMap(() => combineLatest([ @@ -712,7 +699,6 @@ export class VaultComponent implements OnInit, OnDestr async handleUnknownCipher() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("unknownCipher"), }); await this.router.navigate([], { @@ -842,9 +828,13 @@ export class VaultComponent implements OnInit, OnDestr if (orgId == null) { orgId = "MyVault"; } - const orgs = await firstValueFrom(this.filterComponent.filters.organizationFilter.data$); + const data = this.filterComponent()?.filters?.organizationFilter?.data$; + if (data == undefined) { + return; + } + const orgs = await firstValueFrom(data); const orgNode = ServiceUtils.getTreeNodeObject(orgs, orgId) as TreeNode; - await this.filterComponent.filters?.organizationFilter?.action(orgNode); + await this.filterComponent()?.filters?.organizationFilter?.action(orgNode); } addFolder = (): void => { @@ -887,7 +877,10 @@ export class VaultComponent implements OnInit, OnDestr */ async editCipherAttachments(cipher: C) { if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - await this.go({ cipherId: null, itemId: null }); + await this.go( + { cipherId: null, itemId: null }, + this.configureRouterFocusToCipher(typeof cipher?.id === "string" ? cipher.id : undefined), + ); return; } @@ -912,7 +905,10 @@ export class VaultComponent implements OnInit, OnDestr canEditCipher: cipher.edit, }); - const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed); + const result = await lastValueFrom(dialogRef.closed); + if (result === undefined) { + return; + } if ( result.action === AttachmentDialogResult.Uploaded || @@ -957,7 +953,10 @@ export class VaultComponent implements OnInit, OnDestr } // Clear the query params when the dialog closes - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(formConfig.originalCipher?.id), + ); } /** @@ -966,7 +965,7 @@ export class VaultComponent implements OnInit, OnDestr */ async addCipher(cipherType?: CipherType) { const type = cipherType ?? this.activeFilter.cipherType; - const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type); + const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", undefined, type); const collectionId = this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null ? this.activeFilter.collectionId @@ -994,7 +993,7 @@ export class VaultComponent implements OnInit, OnDestr } async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) { - return this.editCipherId(uuidAsString(cipher?.id), cloneMode); + return this.editCipherId(uuidAsString(cipher.id), cloneMode); } /** @@ -1017,7 +1016,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // didn't pass password prompt, so don't open add / edit modal - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1059,7 +1061,10 @@ export class VaultComponent implements OnInit, OnDestr !(await this.passwordRepromptService.showPasswordPrompt()) ) { // Didn't pass password prompt, so don't open add / edit modal. - await this.go({ cipherId: null, itemId: null, action: null }); + await this.go( + { cipherId: null, itemId: null, action: null }, + this.configureRouterFocusToCipher(cipher.id), + ); return; } @@ -1088,6 +1093,9 @@ export class VaultComponent implements OnInit, OnDestr }, }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } if (result.action === CollectionDialogAction.Saved) { if (result.collection) { // Update CollectionService with the new collection @@ -1104,7 +1112,7 @@ export class VaultComponent implements OnInit, OnDestr async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise { const dialog = openCollectionDialog(this.dialogService, { data: { - collectionId: c?.id, + collectionId: c.id, organizationId: c.organizationId, initialTab: tab, limitNestedCollections: true, @@ -1112,6 +1120,9 @@ export class VaultComponent implements OnInit, OnDestr }); const result = await lastValueFrom(dialog.closed); + if (result === undefined) { + return; + } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (result.action === CollectionDialogAction.Saved) { if (result.collection) { @@ -1163,7 +1174,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t("deletedCollectionId", collection.name), }); if (navigateAway) { @@ -1195,13 +1205,12 @@ export class VaultComponent implements OnInit, OnDestr let availableCollections: CollectionView[] = []; const orgId = - this.activeFilter.organizationId || - ciphers.find((c) => c.organizationId !== null)?.organizationId; + this.activeFilter.organizationId || ciphers.find((c) => !!c.organizationId)?.organizationId; if (orgId && orgId !== "MyVault") { const organization = this.allOrganizations.find((o) => o.id === orgId); availableCollections = this.allCollections.filter( - (c) => c.organizationId === organization.id, + (c) => c.organizationId === organization?.id, ); } @@ -1229,7 +1238,7 @@ export class VaultComponent implements OnInit, OnDestr ciphers: ciphersToAssign, organizationId: orgId as OrganizationId, availableCollections, - activeCollection: this.activeFilter?.selectedCollectionNode?.node, + activeCollection: this.activeFilter.selectedCollectionNode?.node, }, }); @@ -1255,7 +1264,7 @@ export class VaultComponent implements OnInit, OnDestr await this.editCipher(cipher, true); } - restore = async (c: C): Promise => { + restore = async (c: CipherViewLike) => { let toastMessage; if (!CipherViewLikeUtils.isDeleted(c)) { return; @@ -1281,13 +1290,14 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } catch (e) { this.logService.error(e); + return; } + return; }; async bulkRestore(ciphers: C[]) { @@ -1311,7 +1321,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1321,23 +1330,24 @@ export class VaultComponent implements OnInit, OnDestr await this.cipherService.restoreManyWithServer(selectedCipherIds, activeUserId); this.toastService.showToast({ variant: "success", - title: null, message: toastMessage, }); this.refresh(); } private async handleDeleteEvent(items: VaultItem[]) { - const ciphers: C[] = items.filter((i) => i.collection === undefined).map((i) => i.cipher); - const collections = items.filter((i) => i.cipher === undefined).map((i) => i.collection); + const ciphers = items + .filter((i) => i.collection === undefined && i.cipher !== undefined) + .map((i) => i.cipher as C); + const collections = items + .filter((i) => i.collection !== undefined) + .map((i) => i.collection as CollectionView); if (ciphers.length === 1 && collections.length === 0) { await this.deleteCipher(ciphers[0]); } else if (ciphers.length === 0 && collections.length === 1) { await this.deleteCollection(collections[0]); } else { - const orgIds = items - .filter((i) => i.cipher === undefined) - .map((i) => i.collection.organizationId); + const orgIds = collections.map((c) => c.organizationId); const orgs = await firstValueFrom( this.organizations$.pipe(map((orgs) => orgs.filter((o) => orgIds.includes(o.id)))), ); @@ -1345,7 +1355,7 @@ export class VaultComponent implements OnInit, OnDestr } } - async deleteCipher(c: C): Promise { + async deleteCipher(c: C) { if (!(await this.repromptCipher([c]))) { return; } @@ -1364,7 +1374,7 @@ export class VaultComponent implements OnInit, OnDestr }); if (!confirmed) { - return false; + return; } try { @@ -1373,7 +1383,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem"), }); this.refresh(); @@ -1390,7 +1399,6 @@ export class VaultComponent implements OnInit, OnDestr if (ciphers.length === 0 && collections.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1430,7 +1438,6 @@ export class VaultComponent implements OnInit, OnDestr if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("nothingSelected"), }); return; @@ -1454,11 +1461,8 @@ export class VaultComponent implements OnInit, OnDestr const login = CipherViewLikeUtils.getLogin(cipher); if (!login) { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); + return; } if (field === "username") { @@ -1471,15 +1475,15 @@ export class VaultComponent implements OnInit, OnDestr typeI18nKey = "password"; } else if (field === "totp") { aType = "TOTP"; + if (!login.totp) { + this.showErrorToast(); + return; + } const totpResponse = await firstValueFrom(this.totpService.getCode$(login.totp)); value = totpResponse.code; typeI18nKey = "verificationCodeTotp"; } else { - this.toastService.showToast({ - variant: "error", - title: null, - message: this.i18nService.t("unexpectedError"), - }); + this.showErrorToast(); return; } @@ -1494,10 +1498,13 @@ export class VaultComponent implements OnInit, OnDestr return; } + if (!value) { + this.showErrorToast(); + return; + } this.platformUtilsService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", - title: null, message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), }); @@ -1514,6 +1521,13 @@ export class VaultComponent implements OnInit, OnDestr } } + showErrorToast() { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("unexpectedError"), + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ @@ -1525,7 +1539,6 @@ export class VaultComponent implements OnInit, OnDestr this.toastService.showToast({ variant: "success", - title: null, message: this.i18nService.t( cipherFullView.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites", ), @@ -1540,18 +1553,36 @@ export class VaultComponent implements OnInit, OnDestr : this.cipherService.softDeleteWithServer(id, userId); } - protected async repromptCipher(ciphers: C[]) { + protected async repromptCipher(ciphers: CipherViewLike[]) { const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); } private refresh() { - this.refresh$.next(); - this.vaultItemsComponent?.clearSelection(); + this.refresh$.next(undefined); + this.vaultItemsComponent()?.clearSelection(); } - private async go(queryParams: any = null) { + /** + * Helper function to set up the `state.focusAfterNav` property for dialog router navigation if + * the cipherId exists. If it doesn't exist, returns undefined. + * + * This ensures that when the routed dialog is closed, the focus returns to the cipher button in + * the vault table, which allows keyboard users to continue navigating uninterrupted. + * + * @param cipherId id of cipher + * @returns Partial, specifically the state.focusAfterNav property, or undefined + */ + private configureRouterFocusToCipher(cipherId?: string): Partial | undefined { + if (cipherId) { + return { + state: { focusAfterNav: `#cipher-btn-${cipherId}` }, + }; + } + } + + private async go(queryParams: any = null, navigateOptions?: NavigationExtras) { if (queryParams == null) { queryParams = { favorites: this.activeFilter.isFavorites || null, @@ -1567,13 +1598,13 @@ export class VaultComponent implements OnInit, OnDestr queryParams: queryParams, queryParamsHandling: "merge", replaceUrl: true, + ...navigateOptions, }); } private showMissingPermissionsError() { this.toastService.showToast({ variant: "error", - title: null, message: this.i18nService.t("missingPermissions"), }); } @@ -1584,13 +1615,13 @@ export class VaultComponent implements OnInit, OnDestr */ private async getPasswordFromCipherViewLike(cipher: C): Promise { if (!CipherViewLikeUtils.isCipherListView(cipher)) { - return Promise.resolve(cipher.login?.password); + return Promise.resolve(cipher?.login?.password); } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); - return cipherView.login?.password; + return cipherView.login.password; } } diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts new file mode 100644 index 00000000000..4a8865990df --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.spec.ts @@ -0,0 +1,269 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; + +describe("WebVaultExtensionPromptService", () => { + let service: WebVaultExtensionPromptService; + + const mockUserId = "user-123" as UserId; + const mockAccountCreationDate = new Date("2026-01-15"); + + const getFeatureFlag = jest.fn(); + const extensionInstalled$ = new BehaviorSubject(false); + const mockStateSubject = new BehaviorSubject(false); + const activeAccountSubject = new BehaviorSubject<{ id: UserId; creationDate: Date | null }>({ + id: mockUserId, + creationDate: mockAccountCreationDate, + }); + const getUser = jest.fn().mockReturnValue({ state$: mockStateSubject.asObservable() }); + + beforeEach(() => { + jest.clearAllMocks(); + getFeatureFlag.mockResolvedValue(false); + extensionInstalled$.next(false); + mockStateSubject.next(false); + activeAccountSubject.next({ id: mockUserId, creationDate: mockAccountCreationDate }); + + TestBed.configureTestingModule({ + providers: [ + WebVaultExtensionPromptService, + { + provide: StateProvider, + useValue: { + getUser, + }, + }, + { + provide: WebBrowserInteractionService, + useValue: { + extensionInstalled$: extensionInstalled$.asObservable(), + }, + }, + { + provide: AccountService, + useValue: { + activeAccount$: activeAccountSubject.asObservable(), + }, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag, + }, + }, + { + provide: DialogService, + useValue: { + open: jest.fn(), + }, + }, + ], + }); + + service = TestBed.inject(WebVaultExtensionPromptService); + }); + + describe("conditionallyPromptUserForExtension", () => { + it("returns false when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + expect(getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + }); + + it("returns false when dialog has been dismissed", async () => { + getFeatureFlag.mockResolvedValueOnce(true); + mockStateSubject.next(true); + extensionInstalled$.next(false); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too old)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + const oldAccountDate = new Date("2025-12-01"); // More than 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when profile is not within thresholds (too young)", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(10); // Min age days = 10 + mockStateSubject.next(false); + extensionInstalled$.next(false); + const youngAccountDate = new Date(); // Today + youngAccountDate.setDate(youngAccountDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: youngAccountDate }); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns false when extension is installed", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(true); + + const result = await service.conditionallyPromptUserForExtension(mockUserId); + + expect(result).toBe(false); + }); + + it("returns true and opens dialog when all conditions are met", async () => { + getFeatureFlag + .mockResolvedValueOnce(true) // Main feature flag + .mockResolvedValueOnce(0); // Min age days + mockStateSubject.next(false); + extensionInstalled$.next(false); + + // Set account creation date to be within threshold (15 days old) + const validCreationDate = new Date(); + validCreationDate.setDate(validCreationDate.getDate() - 15); + activeAccountSubject.next({ id: mockUserId, creationDate: validCreationDate }); + + const dialogClosedSubject = new BehaviorSubject(undefined); + const openSpy = jest + .spyOn(WebVaultExtensionPromptDialogComponent, "open") + .mockReturnValue({ closed: dialogClosedSubject.asObservable() } as any); + + const resultPromise = service.conditionallyPromptUserForExtension(mockUserId); + + // Close the dialog + dialogClosedSubject.next(undefined); + + const result = await resultPromise; + + expect(openSpy).toHaveBeenCalledWith(expect.anything()); + expect(result).toBe(true); + }); + }); + + describe("profileIsWithinThresholds", () => { + it("returns false when account is younger than min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns true when account is exactly at min threshold", async () => { + const minAgeDays = 7; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 7); // Exactly 7 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns true when account is within the thresholds", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const validDate = new Date(); + validDate.setDate(validDate.getDate() - 15); // 15 days old (between 0 and 30) + activeAccountSubject.next({ id: mockUserId, creationDate: validDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("returns false when account is older than max threshold (30 days)", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 31); // 31 days old + activeAccountSubject.next({ id: mockUserId, creationDate: oldDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("returns false when account is exactly 30 days old", async () => { + const minAgeDays = 0; + getFeatureFlag.mockResolvedValueOnce(minAgeDays); + + const exactDate = new Date(); + exactDate.setDate(exactDate.getDate() - 30); // Exactly 30 days old + activeAccountSubject.next({ id: mockUserId, creationDate: exactDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + + it("uses default min age of 0 when feature flag is null", async () => { + getFeatureFlag.mockResolvedValueOnce(null); + + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); // 5 days old + activeAccountSubject.next({ id: mockUserId, creationDate: recentDate }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(true); + }); + + it("defaults to false", async () => { + getFeatureFlag.mockResolvedValueOnce(0); + activeAccountSubject.next({ id: mockUserId, creationDate: null }); + + const result = await service["profileIsWithinThresholds"](); + + expect(result).toBe(false); + }); + }); + + describe("getDialogDismissedState", () => { + it("returns the SingleUserState for the dialog dismissed state", () => { + service.getDialogDismissedState(mockUserId); + + expect(getUser).toHaveBeenCalledWith( + mockUserId, + expect.objectContaining({ + key: "vaultWelcomeExtensionDialogDismissed", + }), + ); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts new file mode 100644 index 00000000000..3e13935f94c --- /dev/null +++ b/apps/web/src/app/vault/services/web-vault-extension-prompt.service.ts @@ -0,0 +1,104 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, WELCOME_EXTENSION_DIALOG_DISK } from "@bitwarden/state"; + +import { WebVaultExtensionPromptDialogComponent } from "../components/web-vault-extension-prompt/web-vault-extension-prompt-dialog.component"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; + +export const WELCOME_EXTENSION_DIALOG_DISMISSED = new UserKeyDefinition( + WELCOME_EXTENSION_DIALOG_DISK, + "vaultWelcomeExtensionDialogDismissed", + { + deserializer: (dismissed) => dismissed, + clearOn: [], + }, +); + +@Injectable({ providedIn: "root" }) +export class WebVaultExtensionPromptService { + private stateProvider = inject(StateProvider); + private webBrowserInteractionService = inject(WebBrowserInteractionService); + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + + /** + * Conditionally prompts the user to install the web extension + */ + async conditionallyPromptUserForExtension(userId: UserId) { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_WelcomeDialogWithExtensionPrompt, + ); + + if (!featureFlagEnabled) { + return false; + } + + // Extension check takes time, trigger it early + const hasExtensionInstalled = firstValueFrom( + this.webBrowserInteractionService.extensionInstalled$, + ); + + const hasDismissedExtensionPrompt = await firstValueFrom( + this.getDialogDismissedState(userId).state$.pipe(map((dismissed) => dismissed ?? false)), + ); + if (hasDismissedExtensionPrompt) { + return false; + } + + const profileIsWithinThresholds = await this.profileIsWithinThresholds(); + if (!profileIsWithinThresholds) { + return false; + } + + if (await hasExtensionInstalled) { + return false; + } + + const dialogRef = WebVaultExtensionPromptDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return true; + } + + /** Returns the SingleUserState for the dialog dismissed state */ + getDialogDismissedState(userId: UserId) { + return this.stateProvider.getUser(userId, WELCOME_EXTENSION_DIALOG_DISMISSED); + } + + /** + * Returns true if the user's profile is within the defined thresholds for showing the extension prompt, false otherwise. + * Thresholds are defined as account age between a configurable number of days and 30 days. + */ + private async profileIsWithinThresholds() { + const creationDate = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.creationDate)), + ); + + // When account or creationDate is not available for some reason, + // default to not showing the prompt to avoid disrupting the user. + if (!creationDate) { + return false; + } + + const minAccountAgeDays = await this.configService.getFeatureFlag( + FeatureFlag.PM29438_DialogWithExtensionPromptAccountAge, + ); + + const now = new Date(); + const accountAgeMs = now.getTime() - creationDate.getTime(); + const accountAgeDays = accountAgeMs / (1000 * 60 * 60 * 24); + + const minAgeDays = minAccountAgeDays ?? 0; + const maxAgeDays = 30; + + return accountAgeDays >= minAgeDays && accountAgeDays < maxAgeDays; + } +} diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts index a224b8e7c4b..14bbc1a86d5 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.spec.ts @@ -7,7 +7,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService } from "@bitwarden/components"; @@ -20,7 +20,9 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; import { WebVaultPromptService } from "./web-vault-prompt.service"; +import { WelcomeDialogService } from "./welcome-dialog.service"; describe("WebVaultPromptService", () => { let service: WebVaultPromptService; @@ -38,20 +40,34 @@ describe("WebVaultPromptService", () => { ); const upsertAutoConfirm = jest.fn().mockResolvedValue(undefined); const organizations$ = jest.fn().mockReturnValue(of([])); - const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(undefined); + const displayUpgradePromptConditionally = jest.fn().mockResolvedValue(false); const enforceOrganizationDataOwnership = jest.fn().mockResolvedValue(undefined); + const conditionallyShowWelcomeDialog = jest.fn().mockResolvedValue(false); const logError = jest.fn(); + const conditionallyPromptUserForExtension = jest.fn().mockResolvedValue(false); + + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } beforeEach(() => { jest.clearAllMocks(); + activeAccount$ = new BehaviorSubject(createAccount()); + TestBed.configureTestingModule({ providers: [ WebVaultPromptService, { provide: UnifiedUpgradePromptService, useValue: { displayUpgradePromptConditionally } }, { provide: VaultItemsTransferService, useValue: { enforceOrganizationDataOwnership } }, { provide: PolicyService, useValue: { policies$ } }, - { provide: AccountService, useValue: { activeAccount$: of({ id: mockUserId }) } }, + { provide: AccountService, useValue: { activeAccount$ } }, { provide: AutomaticUserConfirmationService, useValue: { configuration$: configurationAutoConfirm$, upsert: upsertAutoConfirm }, @@ -60,6 +76,14 @@ describe("WebVaultPromptService", () => { { provide: ConfigService, useValue: { getFeatureFlag$ } }, { provide: DialogService, useValue: { open } }, { provide: LogService, useValue: { error: logError } }, + { + provide: WebVaultExtensionPromptService, + useValue: { conditionallyPromptUserForExtension }, + }, + { + provide: WelcomeDialogService, + useValue: { conditionallyShowWelcomeDialog, conditionallyPromptUserForExtension }, + }, ], }); @@ -82,11 +106,19 @@ describe("WebVaultPromptService", () => { service["vaultItemTransferService"].enforceOrganizationDataOwnership, ).toHaveBeenCalledWith(mockUserId); }); + + it("calls conditionallyPromptUserForExtension with the userId", async () => { + await service.conditionallyPromptUser(); + + expect( + service["webVaultExtensionPromptService"].conditionallyPromptUserForExtension, + ).toHaveBeenCalledWith(mockUserId); + }); }); describe("setupAutoConfirm", () => { it("shows dialog when all conditions are met", fakeAsync(() => { - getFeatureFlag$.mockReturnValueOnce(of(true)); + getFeatureFlag$.mockReturnValue(of(true)); configurationAutoConfirm$.mockReturnValueOnce( of({ showSetupDialog: true, enabled: false, showBrowserNotification: false }), ); diff --git a/apps/web/src/app/vault/services/web-vault-prompt.service.ts b/apps/web/src/app/vault/services/web-vault-prompt.service.ts index 1774bfcc085..aac30e7d0f4 100644 --- a/apps/web/src/app/vault/services/web-vault-prompt.service.ts +++ b/apps/web/src/app/vault/services/web-vault-prompt.service.ts @@ -20,6 +20,9 @@ import { } from "../../admin-console/organizations/policies"; import { UnifiedUpgradePromptService } from "../../billing/individual/upgrade/services"; +import { WebVaultExtensionPromptService } from "./web-vault-extension-prompt.service"; +import { WelcomeDialogService } from "./welcome-dialog.service"; + @Injectable() export class WebVaultPromptService { private unifiedUpgradePromptService = inject(UnifiedUpgradePromptService); @@ -31,6 +34,8 @@ export class WebVaultPromptService { private configService = inject(ConfigService); private dialogService = inject(DialogService); private logService = inject(LogService); + private webVaultExtensionPromptService = inject(WebVaultExtensionPromptService); + private welcomeDialogService = inject(WelcomeDialogService); private userId$ = this.accountService.activeAccount$.pipe(getUserId); @@ -46,9 +51,15 @@ export class WebVaultPromptService { async conditionallyPromptUser() { const userId = await firstValueFrom(this.userId$); - void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + if (await this.unifiedUpgradePromptService.displayUpgradePromptConditionally()) { + return; + } - void this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + await this.vaultItemTransferService.enforceOrganizationDataOwnership(userId); + + await this.welcomeDialogService.conditionallyShowWelcomeDialog(); + + await this.webVaultExtensionPromptService.conditionallyPromptUserForExtension(userId); this.checkForAutoConfirm(); } diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts new file mode 100644 index 00000000000..752514ca066 --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.spec.ts @@ -0,0 +1,123 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogRef, DialogService } from "@bitwarden/components"; +import { StateProvider } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +import { WelcomeDialogService } from "./welcome-dialog.service"; + +describe("WelcomeDialogService", () => { + let service: WelcomeDialogService; + + const mockUserId = "user-123" as UserId; + + const getFeatureFlag = jest.fn().mockResolvedValue(false); + const getUserState$ = jest.fn().mockReturnValue(of(false)); + const mockDialogOpen = jest.spyOn(VaultWelcomeDialogComponent, "open"); + + let activeAccount$: BehaviorSubject; + + function createAccount(overrides: Partial = {}): Account { + return { + id: mockUserId, + creationDate: new Date(), + ...overrides, + } as Account; + } + + beforeEach(() => { + jest.clearAllMocks(); + mockDialogOpen.mockReset(); + + activeAccount$ = new BehaviorSubject(createAccount()); + + TestBed.configureTestingModule({ + providers: [ + WelcomeDialogService, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: ConfigService, useValue: { getFeatureFlag } }, + { provide: DialogService, useValue: {} }, + { provide: StateProvider, useValue: { getUserState$ } }, + ], + }); + + service = TestBed.inject(WelcomeDialogService); + }); + + describe("conditionallyShowWelcomeDialog", () => { + it("should not show dialog when no active account", async () => { + activeAccount$.next(null); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when feature flag is disabled", async () => { + getFeatureFlag.mockResolvedValueOnce(false); + + await service.conditionallyShowWelcomeDialog(); + + expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM29437_WelcomeDialog); + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account has no creation date", async () => { + activeAccount$.next(createAccount({ creationDate: undefined })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when account is older than 30 days", async () => { + const overThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 - 1000); + activeAccount$.next(createAccount({ creationDate: overThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when user has already acknowledged it", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(true)); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should show dialog for new user who has not acknowledged", async () => { + activeAccount$.next(createAccount({ creationDate: new Date() })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + + it("should show dialog for account created exactly 30 days ago", async () => { + const exactlyThirtyDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); + activeAccount$.next(createAccount({ creationDate: exactlyThirtyDaysAgo })); + getFeatureFlag.mockResolvedValueOnce(true); + getUserState$.mockReturnValueOnce(of(false)); + mockDialogOpen.mockReturnValue({ closed: of(undefined) } as DialogRef); + + await service.conditionallyShowWelcomeDialog(); + + expect(mockDialogOpen).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/vault/services/welcome-dialog.service.ts b/apps/web/src/app/vault/services/welcome-dialog.service.ts new file mode 100644 index 00000000000..25b24b6df2d --- /dev/null +++ b/apps/web/src/app/vault/services/welcome-dialog.service.ts @@ -0,0 +1,72 @@ +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DialogService } from "@bitwarden/components"; +import { StateProvider, UserKeyDefinition, VAULT_WELCOME_DIALOG_DISK } from "@bitwarden/state"; + +import { VaultWelcomeDialogComponent } from "../components/vault-welcome-dialog/vault-welcome-dialog.component"; + +const VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY = new UserKeyDefinition( + VAULT_WELCOME_DIALOG_DISK, + "vaultWelcomeDialogAcknowledged", + { + deserializer: (value) => value, + clearOn: [], + }, +); + +const THIRTY_DAY_MS = 1000 * 60 * 60 * 24 * 30; + +@Injectable({ providedIn: "root" }) +export class WelcomeDialogService { + private accountService = inject(AccountService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + private stateProvider = inject(StateProvider); + + /** + * Conditionally shows the welcome dialog to new users. + * + * @returns true if the dialog was shown, false otherwise + */ + async conditionallyShowWelcomeDialog() { + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return; + } + + const enabled = await this.configService.getFeatureFlag(FeatureFlag.PM29437_WelcomeDialog); + if (!enabled) { + return; + } + + const createdAt = account.creationDate; + if (!createdAt) { + return; + } + + const ageMs = Date.now() - createdAt.getTime(); + const isNewUser = ageMs >= 0 && ageMs <= THIRTY_DAY_MS; + if (!isNewUser) { + return; + } + + const acknowledged = await firstValueFrom( + this.stateProvider + .getUserState$(VAULT_WELCOME_DIALOG_ACKNOWLEDGED_KEY, account.id) + .pipe(map((v) => v ?? false)), + ); + + if (acknowledged) { + return; + } + + const dialogRef = VaultWelcomeDialogComponent.open(this.dialogService); + await firstValueFrom(dialogRef.closed); + + return; + } +} diff --git a/apps/web/src/images/integrations/logo-huntress-siem-darkmode.svg b/apps/web/src/images/integrations/logo-huntress-siem-darkmode.svg new file mode 100644 index 00000000000..c25edc826e9 --- /dev/null +++ b/apps/web/src/images/integrations/logo-huntress-siem-darkmode.svg @@ -0,0 +1,63 @@ + + + + + + + diff --git a/apps/web/src/images/integrations/logo-huntress-siem.svg b/apps/web/src/images/integrations/logo-huntress-siem.svg index 06f2a3443c0..99c63a850f8 100644 --- a/apps/web/src/images/integrations/logo-huntress-siem.svg +++ b/apps/web/src/images/integrations/logo-huntress-siem.svg @@ -1 +1,72 @@ - \ No newline at end of file + + + diff --git a/apps/web/src/images/vault/extension-mock-login.png b/apps/web/src/images/vault/extension-mock-login.png new file mode 100644 index 00000000000..e002da6db2d Binary files /dev/null and b/apps/web/src/images/vault/extension-mock-login.png differ diff --git a/apps/web/src/images/welcome-dialog-graphic.png b/apps/web/src/images/welcome-dialog-graphic.png new file mode 100644 index 00000000000..fd2a12c5272 Binary files /dev/null and b/apps/web/src/images/welcome-dialog-graphic.png differ diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 7b6c7778d70..1e12ff7be2d 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker $ID$ gewysig.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Hierdie send is by verstek versteek. U kan sy sigbaarheid wissel deur die knop hier onder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Laai aanhegsels af" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persoonlike eienaarskap" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Die Bitwarden-gebruiker wat hierdie Send geskep het, het gekies om hul e-posadres te verberg. U moet verseker dat u die bron van hierdie skakel vertrou voordat u die inhoud gebruik of aflaai.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 65b8578a206..9ceeb66bf59 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "لا تعرف كلمة المرور؟ اطلب من المرسل كلمة المرور المطلوبة للوصول إلى هذا الإرسال.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "هذا الإرسال مخفي بشكل افتراضي. يمكنك تبديل الرؤية باستخدام الزر أدناه.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "تحميل المرفقات" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 6c2150ec158..7d25a8a86e0 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -48,7 +48,7 @@ "message": "Bu bəndə düzəliş etmə icazəniz yoxdur" }, "reviewAccessIntelligence": { - "message": "Review security reports to find and fix credential risks before they escalate." + "message": "Kimlik məlumatları riskləri artmazdan əvvəl onları tapıb düzəltmək üçün təhlükəsizlik hesabatlarını incələyin." }, "reviewAtRiskLoginsPrompt": { "message": "Risk altında olan girişi nəzərdən keçirin" @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ tətbiq kritik olaraq işarələndi", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ tətbiq kritik deyil olaraq işarələndi", "placeholders": { "count": { "content": "$1", @@ -287,7 +287,7 @@ } }, "markAppCountAsCritical": { - "message": "Mark $COUNT$ as critical", + "message": "$COUNT$ tətbiqi kritik olaraq işarələ", "placeholders": { "count": { "content": "$1", @@ -296,7 +296,7 @@ } }, "markAppCountAsNotCritical": { - "message": "Mark $COUNT$ as not critical", + "message": "$COUNT$ tətbiqi kritik deyil olaraq işarələ", "placeholders": { "count": { "content": "$1", @@ -1438,7 +1438,7 @@ "message": "Keçidə sahib olan hər kəs" }, "anyOneWithPassword": { - "message": "Sizin təyin etdiyiniz parola sahib hər kəs" + "message": "Ayarladığınız parola sahib hər kəs" }, "location": { "message": "Yerləşmə" @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abunəliyiniz əvvəlki halına qaytarıldı." }, + "resubscribe": { + "message": "Təkrar abunə ol" + }, + "yourSubscriptionIsExpired": { + "message": "Abunəliyiniz bitib" + }, + "yourSubscriptionIsCanceled": { + "message": "Abunəliyiniz ləğv edilib" + }, "cancelConfirmation": { "message": "İmtina etmək istədiyinizə əminsiniz? Bu faktura dövrünün sonunda bu abunəliyin bütün özəlliklərinə erişimi itirəcəksiniz." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ istifadəçisinə düzəliş edildi.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence-niz yaradılır..." }, - "fetchingMemberData": { - "message": "Üzv veriləri alınır..." - }, - "analyzingPasswordHealth": { - "message": "Parol sağlamlığı analiz edirilir..." - }, - "calculatingRiskScores": { - "message": "Risk xalı hesablanır..." - }, - "generatingReportData": { - "message": "Hesabat veriləri yaradılır..." - }, - "savingReport": { - "message": "Hesabat saxlanılır..." - }, - "compilingInsights": { - "message": "Təhlillər şərh edilir..." - }, "loadingProgress": { "message": "İrəliləyiş yüklənir" }, - "thisMightTakeFewMinutes": { - "message": "Bu, bir neçə dəqiqə çəkə bilər." + "reviewingMemberData": { + "message": "Üzv veriləri incələnir..." + }, + "analyzingPasswords": { + "message": "Parollar təhlil edilir..." + }, + "calculatingRisks": { + "message": "Risklər hesablanır..." + }, + "generatingReports": { + "message": "Hesabatlar yaradılır..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Hesabatı işə sal" @@ -5431,7 +5446,7 @@ "message": "Minimum söz sayı" }, "passwordTypePolicyOverride": { - "message": "Password type", + "message": "Parol növü", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5704,7 +5719,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendCreatedDescriptionV2": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5714,7 +5729,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Bu Send keçidini kopyala və paylaş. Send, təyin etdiyiniz keçidə və parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", + "message": "Bu Send keçidini kopyala və paylaş. Send, keçidə və ayarladığınız parola sahib olan hər kəs üçün növbəti $TIME$ ərzində əlçatan olacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5840,10 +5855,6 @@ "message": "Parolu bilmirsiniz? Bu \"Send\"ə erişmək üçün parolu göndərən şəxsdən istəyin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu \"send\" ilkin olaraq gizlidir. Aşağıdakı düyməni istifadə edərək görünməni dəyişdirə bilərsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Qoşmaları endir" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskləri və siyasət güncəlləmələrini qəbul edirəm" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fərdi seyfi xaric et" }, @@ -6429,6 +6455,10 @@ "message": "\"Send\"ə bax", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Bu \"Send\"ə baxmaq üçün e-poçtunuzu doğrulayın", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bu \"Send\"i yaradan Bitwarden istifadəçisi e-poçt ünvanını gizlətməyi seçib. İstifadə etməzdən və ya endirməzdən əvvəl bu keçidin mənbəyinin etibarlı olduğuna əmin olmalısınız.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6596,7 +6626,7 @@ "message": "Yeni istifadəçiləri avtomatik qeydiyyata al" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "Bu təşkilat, sizi \"parol sıfırlama\"da avtomatik olaraq qeydiyyata alan müəssisə siyasətinə sahibdir. Qeydiyyat, təşkilat administratorlarına ana parolunuzu dəyişdirmə icazəsi verəcək." + "message": "Bu təşkilat, sizi \"parol sıfırlama\"da avtomatik olaraq qeydiyyata alan Enterprise siyasətinə sahibdir. Qeydiyyat, təşkilat inzibatçılarına ana parolunuzu dəyişdirmə icazəsi verəcək." }, "resetPasswordOrgKeysError": { "message": "\"Təşkilat açarları\" cavabı boşdur" @@ -6674,10 +6704,10 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 dəvət göndərilib" }, "bulkReinviteSentToast": { - "message": "$COUNT$ invitations sent", + "message": "$COUNT$ dəvət göndərilib", "placeholders": { "count": { "content": "$1", @@ -6703,7 +6733,7 @@ } }, "bulkReinviteProgressTitle": { - "message": "$COUNT$ of $TOTAL$ invitations sent...", + "message": "$COUNT$/$TOTAL$ dəvət göndərilib...", "placeholders": { "count": { "content": "$1", @@ -6716,10 +6746,10 @@ } }, "bulkReinviteProgressSubtitle": { - "message": "Keep this page open until all are sent." + "message": "Hamısı göndərilənə qədər bu səhifəni açıq saxlayın." }, "bulkReinviteFailuresTitle": { - "message": "$COUNT$ invitations didn't send", + "message": "$COUNT$ dəvət göndərilməyib", "placeholders": { "count": { "content": "$1", @@ -6728,10 +6758,10 @@ } }, "bulkReinviteFailureTitle": { - "message": "1 invitation didn't send" + "message": "1 dəvət göndərilməyib" }, "bulkReinviteFailureDescription": { - "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "message": "$COUNT$/$TOTAL$ üzvə dəvət göndərərkən xəta baş verdi. Yenidən göndərməyə çalışın, problem davam edərsə,", "placeholders": { "count": { "content": "$1", @@ -6744,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Yenidən göndərməyə çalış" }, "bulkRemovedMessage": { "message": "Uğurla çıxarıldı" @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domeni" }, @@ -10185,7 +10218,7 @@ "message": "Tapşırıq təyin et" }, "allTasksAssigned": { - "message": "All tasks have been assigned" + "message": "Bütün tapşırıqlar təyin edilib" }, "assignSecurityTasksToMembers": { "message": "Parol dəyişdirmə bildirişlərini göndər" @@ -11896,13 +11929,10 @@ "noItemsInArchiveDesc": { "message": "Arxivlənmiş elementlər burada görünəcək, ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək." }, - "itemWasSentToArchive": { - "message": "Element arxivə göndərildi" + "itemArchiveToast": { + "message": "Element arxivləndi" }, - "itemWasUnarchived": { - "message": "Element arxivdən çıxarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Element arxivdən çıxarıldı" }, "bulkArchiveItems": { @@ -12583,7 +12613,7 @@ "message": "Davam etmək istədiyinizə əminsiniz?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Xəta: Şifrəsi açıla bilmir" }, "userVerificationFailed": { "message": "İstifadəçi doğrulaması uğursuz oldu." @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Bütün $GB$ GB-lıq şifrələnmiş anbar sahənizi istifadə etmisiniz. Faylları saxlaya bilmək üçün daha çox anbar sahəsi əlavə edin." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kimlər baxa bilər" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Yararsız Send parolu" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Şəxslər, Send-ə baxması üçün parolu daxil etməli olacaqlar", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12891,22 +12951,22 @@ "message": "bir istifadəçi üçün" }, "upgradeToTeams": { - "message": "Upgrade to Teams" + "message": "\"Teams\"ə yüksəlt" }, "upgradeToEnterprise": { - "message": "Upgrade to Enterprise" + "message": "\"Enterprise\"a yüksəlt" }, "upgradeShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + "message": "Ailələr planı ilə daha çoxunu paylaşın, ya da Komandalar və ya Müəssisə planı ilə daha güclü və etibarlı parol təhlükəsizliyi əldə edin." }, "organizationUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Qiymətə vergi daxil deyildir və illik hesablanır." }, "invoicePreviewErrorMessage": { - "message": "Encountered an error while generating the invoice preview." + "message": "Faktura önizləməsini yaradan zaman xəta ilə üzləşildi." }, "planProratedMembershipInMonths": { - "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "message": "Proporsional $PLAN$ üzvlüyü ($NUMOFMOONTHS$)", "placeholders": { "plan": { "content": "$1", @@ -12919,16 +12979,16 @@ } }, "premiumSubscriptionCredit": { - "message": "Premium subscription credit" + "message": "Premium abunəlik krediti" }, "enterpriseMembership": { - "message": "Enterprise membership" + "message": "Enterprise üzvlüyü" }, "teamsMembership": { - "message": "Teams membership" + "message": "Teams üzvlüyü" }, "plansUpdated": { - "message": "You've upgraded to $PLAN$!", + "message": "$PLAN$ planına yüksəltmisiniz!", "placeholders": { "plan": { "content": "$1", @@ -12937,6 +12997,24 @@ } }, "paymentMethodUpdateError": { - "message": "There was an error updating your payment method." + "message": "Ödəniş üsulunuzu güncəlləyərkən xəta baş verdi." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Bu \"Send\"in müddəti bitir: $TIME$ $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index ef00d46b4c0..a0d13b0f5db 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Падпіска была адноўлена." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Вы сапраўды хочаце скасаваць? Вы страціце доступ да ўсіх функцый падпіскі ў канцы гэтага плацежнага перыяду." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Карыстальнік $ID$ адрэдагаваны.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Не ведаеце пароль? Спытайце ў адпраўніка пароль, які неабходны для доступу да гэтага Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Прадвызначана гэты Send схаваны. Вы можаце змяніць яго бачнасць выкарыстоўваючы кнопку ніжэй.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Выдаліць асабістае сховішча" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Карыстальнік Bitwarden, які стварыў гэты Send, вырашыў схаваць сваю электронную пошту. Пераканайцеся, што гэта спасылка атрымана з надзейных крыніц і толькі пасля гэтага выкарыстоўвайце або пампуйце дадзенае змесціва.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Памылковы праверачны код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index e32b2b3d5f1..caef0b4869b 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Абонаментът е подновен." }, + "resubscribe": { + "message": "Подновявана на абонамента" + }, + "yourSubscriptionIsExpired": { + "message": "Абонаментът Ви е изтекъл" + }, + "yourSubscriptionIsCanceled": { + "message": "Абонаментът Ви е прекратен" + }, "cancelConfirmation": { "message": "Уверени ли сте, че искате да прекратите абонамента? След края на последно платения период ще загубите всички допълнителни преимущества." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Автоматично потвърден потребител: $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Потребител № $ID$ е редактиран.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Създаване на Вашия анализ на достъпа…" }, - "fetchingMemberData": { - "message": "Извличане на данните за членовете…" - }, - "analyzingPasswordHealth": { - "message": "Анализиране на състоянието на паролите…" - }, - "calculatingRiskScores": { - "message": "Изчисляване на оценките на риска…" - }, - "generatingReportData": { - "message": "Създаване на данните за доклада…" - }, - "savingReport": { - "message": "Запазване на доклада…" - }, - "compilingInsights": { - "message": "Събиране на подробности…" - }, "loadingProgress": { "message": "Зареждане на напредъка" }, - "thisMightTakeFewMinutes": { - "message": "Това може да отнеме няколко минути." + "reviewingMemberData": { + "message": "Преглеждане на данните за членовете…" + }, + "analyzingPasswords": { + "message": "Анализиране на паролите…" + }, + "calculatingRisks": { + "message": "Изчисляване на рисковете…" + }, + "generatingReports": { + "message": "Създаване на доклади…" + }, + "compilingInsightsProgress": { + "message": "Събиране на подробности…" + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Изпълнение на доклада" @@ -5840,10 +5855,6 @@ "message": "Ако не знаете паролата, поискайте от изпращача да ви я даде.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Стандартно изпращането е скрито. Може да промените това като натиснете бутона по-долу.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Сваляне на прикачените файлове" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Приемам тези рискове и промени в политиката" }, + "autoConfirmEnabledByAdmin": { + "message": "Настройката за автоматично потвърждаване на потребители е включена" + }, + "autoConfirmDisabledByAdmin": { + "message": "Настройката за автоматично потвърждаване на потребители е изключена" + }, + "autoConfirmEnabledByPortal": { + "message": "Добавена е политика за автоматично потвърждаване на потребителите" + }, + "autoConfirmDisabledByPortal": { + "message": "Премахната е политика за автоматично потвърждаване на потребителите" + }, + "system": { + "message": "Система" + }, "personalOwnership": { "message": "Индивидуално притежание" }, @@ -6429,6 +6455,10 @@ "message": "Преглед на изпращането", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Потвърдете е-пощата си, за да видите това Изпращане", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Потребителят, който е създал това изпращане, е избрал да скрие адреса на своята е-поща. Уверете се, че източникът на тази връзка е достоверен, преди да я последвате или да свалите съдържание чрез нея.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, + "invalidEmailOrVerificationCode": { + "message": "Грешна е-поща или код за потвърждаване" + }, "keyConnectorDomain": { "message": "Домейн на конектора за ключове" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Архивираните елементи ще се показват тук и ще бъдат изключени от общите резултати при търсене и от предложенията за автоматично попълване." }, - "itemWasSentToArchive": { - "message": "Елементът беше преместен в архива" + "itemArchiveToast": { + "message": "Елементът е преместен в архива" }, - "itemWasUnarchived": { - "message": "Елементът беше изваден от архива" - }, - "itemUnarchived": { - "message": "Елементът беше изваден от архива" + "itemUnarchivedToast": { + "message": "Елементът е изваден от архива" }, "bulkArchiveItems": { "message": "Елементите са архивирани" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Използвали сте всичките си $GB$ GB от наличното си място за съхранение на шифровани данни. Ако искате да продължите да добавяте файлове, добавете повече място за съхранение." }, + "extensionPromptHeading": { + "message": "Инсталирайте добавката за по-лесен достъп до трезора си" + }, + "extensionPromptBody": { + "message": "Когато добавката е инсталирана, Битуорден ще бъде винаги лесно достъпен във всички уеб сайтове. С нея можете да попълвате паролите автоматично и да се вписвате с едно щракване на мишката." + }, + "extensionPromptImageAlt": { + "message": "Уеб браузър показващ добавката на Битуорден с елементи за автоматично попълване за текущата уеб страница." + }, + "skip": { + "message": "Пропускане" + }, + "downloadExtension": { + "message": "Сваляне на добавката" + }, "whoCanView": { "message": "Кой може да преглежда" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Можете да въведете повече е-пощи, като ги разделите със запетая." }, + "emailsRequiredChangeAccessType": { + "message": "Потвърждаването на е-пощата изисква да е наличен поне един адрес на е-поща. Ако искате да премахнете всички е-пощи, променете начина за достъп по-горе." + }, "emailPlaceholder": { "message": "потребител@bitwarden.com , потребител@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Неправилна парола за Изпращане" }, + "vaultWelcomeDialogTitle": { + "message": "Влязохте! Добре дошли в Битуорден!" + }, + "vaultWelcomeDialogDescription": { + "message": "Съхранявайте всичките си пароли и лични данни в трезора си в Битуорден. Нека Ви разведем!" + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Начало на обиколката" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Пропускане" + }, "sendPasswordHelperText": { "message": "Хората ще трябва да въведат паролата, за да видят това Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Възникна грешка при обновяването на разплащателния метод." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неправилна парола. Попитайте изпращача за паролата за достъп до това Изпращане.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Това Изпращане изтича в $TIME$ на $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 53f29d874e7..7dd3573e960 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 1ae5523c9ac..8aea40fb633 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 88e4b0569d6..9af40c9f946 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "S'ha restablert la subscripció." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Esteu segur que voleu cancel·lar? Perdreu l'accés a totes les característiques d'aquesta subscripció al final d'aquest cicle de facturació." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "S'ha editat l'usuari $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "No sabeu la contrasenya? Demaneu al remitent la contrasenya necessària per accedir a aquest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixa els fitxers adjunts" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Suprimeix la caixa forta individual" }, @@ -6429,6 +6455,10 @@ "message": "Veure Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'usuari Bitwarden que ha creat aquest Send ha decidit amagar la seua adreça de correu electrònic. Heu d’assegurar-vos que confieu en la font d’aquest enllaç abans d’utilitzar o descarregar el seu contingut.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index d85f3fada18..469ccc0a072 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1925,7 +1925,7 @@ "message": "Ověřovací aplikace" }, "authenticatorAppDescV2": { - "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { @@ -2704,7 +2704,7 @@ "message": "Pokračovat na bitwarden.com?" }, "twoStepContinueToBitwardenUrlDesc": { - "message": "Autentikátor Bitwarden umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" + "message": "Bitwarden Authenticator umožňuje ukládat ověřovací klíče a generovat TOTP kódy pro 2-fázové ověřování. Další informace naleznete na stránkách bitwarden.com" }, "twoStepAuthenticatorScanCodeV2": { "message": "Naskenujte QR kód pomocí Vaší ověřovací aplikace nebo zadejte klíč." @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Předplatné bylo obnoveno." }, + "resubscribe": { + "message": "Obnovit odběr" + }, + "yourSubscriptionIsExpired": { + "message": "Vaše předplatné vypršelo" + }, + "yourSubscriptionIsCanceled": { + "message": "Vaše předplatné je zrušeno" + }, "cancelConfirmation": { "message": "Opravdu chcete zrušit předplatné? Na konci fakturačního období přijdete o veškeré výhody plynoucí z vybraného plánu." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automaticky potvrzený uživatel $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Byl upraven uživatel $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generování Vaší přístupové inteligence..." }, - "fetchingMemberData": { - "message": "Načítání dat člena..." - }, - "analyzingPasswordHealth": { - "message": "Analyzování zdraví hesla..." - }, - "calculatingRiskScores": { - "message": "Výpočet skóre rizik..." - }, - "generatingReportData": { - "message": "Generování dat hlášení..." - }, - "savingReport": { - "message": "Ukládání hlášení..." - }, - "compilingInsights": { - "message": "Sestavování přehledů..." - }, "loadingProgress": { "message": "Průběh načítání" }, - "thisMightTakeFewMinutes": { - "message": "Může to trvat několik minut." + "reviewingMemberData": { + "message": "Přezkoumávání dat členů..." + }, + "analyzingPasswords": { + "message": "Analyzování hesel..." + }, + "calculatingRisks": { + "message": "Výpočet rizik..." + }, + "generatingReports": { + "message": "Generování zpráv..." + }, + "compilingInsightsProgress": { + "message": "Sestavování přehledů..." + }, + "reportGenerationDone": { + "message": "Hotovo!" }, "riskInsightsRunReport": { "message": "Spustit hlášení" @@ -5840,10 +5855,6 @@ "message": "Neznáte heslo? Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je ve výchozím nastavení skrytý. Viditelnost můžete přepnout pomocí tlačítka níže.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stáhnout přílohy" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Přijímám tato rizika a aktualizace zásad" }, + "autoConfirmEnabledByAdmin": { + "message": "Zapnuto Automatické potvrzování uživatele" + }, + "autoConfirmDisabledByAdmin": { + "message": "Vypnuto Automatické potvrzování uživatele" + }, + "autoConfirmEnabledByPortal": { + "message": "Přidána zásada automatického potvrzování uživatele" + }, + "autoConfirmDisabledByPortal": { + "message": "Odebrána zásada automatického potvrzování uživatele" + }, + "system": { + "message": "Systém" + }, "personalOwnership": { "message": "Odebrat osobní trezor" }, @@ -6429,6 +6455,10 @@ "message": "Zobrazit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Ověřte svůj e-mail pro zobrazení tohoto Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Uživatel Bitwardenu, který vytvořil tento Send, se rozhodl skrýt svou e-mailovou adresu. Před použitím nebo stažením jeho obsahu byste se měli ujistit, že zdroji tohoto odkazu důvěřujete.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6744,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Zkusit odeslat znovu" }, "bulkRemovedMessage": { "message": "Úspěšně odebráno" @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, + "invalidEmailOrVerificationCode": { + "message": "Neplatný e-mail nebo ověřovací kód" + }, "keyConnectorDomain": { "message": "Doména Key Connectoru" }, @@ -11896,13 +11929,10 @@ "noItemsInArchiveDesc": { "message": "Zde se zobrazí archivované položky a budou vyloučeny z obecných výsledků vyhledávání a návrhů automatického vyplňování." }, - "itemWasSentToArchive": { - "message": "Položka byla přesunuta do archivu" + "itemArchiveToast": { + "message": "Položka archivována" }, - "itemWasUnarchived": { - "message": "Položka byla odebrána z archivu" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Položka byla odebrána z archivu" }, "bulkArchiveItems": { @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Využili jste celých $GB$ GB Vašeho šifrovaného úložiště. Chcete-li pokračovat v ukládání souborů, přidejte další úložiště." }, + "extensionPromptHeading": { + "message": "Získejte rozšíření pro snadný přístup k trezoru" + }, + "extensionPromptBody": { + "message": "S nainstalovaným rozšířením prohlížeče budete mít Bitwarden všude online. Budou se vyplňovat hesla, takže se můžete přihlásit do svých účtů jediným klepnutím." + }, + "extensionPromptImageAlt": { + "message": "Webový prohlížeč zobrazující rozšíření Bitwarden s položkami automatického vyplňování aktuální webové stránky." + }, + "skip": { + "message": "Přeskočit" + }, + "downloadExtension": { + "message": "Nainstalovat rozšíření" + }, "whoCanView": { "message": "Kdo může zobrazit" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Zadejte více e-mailů oddělených čárkou." }, + "emailsRequiredChangeAccessType": { + "message": "Ověření e-mailu vyžaduje alespoň jednu e-mailovou adresu. Chcete-li odebrat všechny emaily, změňte výše uvedený typ přístupu." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Neplatné heslo k Send" }, + "vaultWelcomeDialogTitle": { + "message": "Jste u nás! Vítejte v Bitwardenu" + }, + "vaultWelcomeDialogDescription": { + "message": "Uložte všechna Vaše hesla a osobní informace v trezoru Bitwarden. Provedeme Vás tady." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Zahájit prohlídku" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Přeskočit" + }, "sendPasswordHelperText": { "message": "Pro zobrazení tohoto Send budou muset jednotlivci zadat heslo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Při aktualizaci Vaší platební metody došlo k chybě." + }, + "sendPasswordInvalidAskOwner": { + "message": "Neplatné heslo. Požádejte odesílatele o heslo potřebné pro přístup k tomuto Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Tento Send vyprší v $TIME$ dne $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index c566ffaf831..d1252309bfc 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 42fbf0cf7c4..11b3dc87b89 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnementet er gentegnet." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Er du sikker på at du vil opsige? Du vil miste adgangen til alle abonnementsfunktionerne ved afslutningen af denne faktureringsperiode." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerede bruger $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Kender ikke adgangskoden? Bed afsenderen om adgangskoden, der kræves for at tilgå denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send er som standard skjult. Dens synlighed kan ændres vha. knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download vedhæftninger" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Fjern individuel boks" }, @@ -6429,6 +6455,10 @@ "message": "Vis Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-brugeren, der oprettede denne Send, har valgt at skjule sin e-mailadresse. Du bør sikre dig, at du stoler på kilden til dette link, inden dets indhold downloades/benyttes.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 89672d615dc..6ea7e3b91a2 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ Anwendungen als kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ Anwendungen als nicht-kritisch markiert", "placeholders": { "count": { "content": "$1", @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Das Abo wurde wieder aufgenommen." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sind Sie sicher, dass Sie kündigen wollen? Am Ende dieses Abrechnungszyklus verlieren Sie den Zugriff auf alle Funktionen dieses Abos." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Benutzer $ID$ bearbeitet.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Deine Access Intelligence wird generiert..." }, - "fetchingMemberData": { - "message": "Mitgliedsdaten werden abgerufen..." - }, - "analyzingPasswordHealth": { - "message": "Passwortsicherheit wird analysiert..." - }, - "calculatingRiskScores": { - "message": "Risikobewertung wird berechnet..." - }, - "generatingReportData": { - "message": "Berichtsdaten werden generiert..." - }, - "savingReport": { - "message": "Bericht wird gespeichert..." - }, - "compilingInsights": { - "message": "Analyse wird zusammengestellt..." - }, "loadingProgress": { "message": "Ladefortschritt" }, - "thisMightTakeFewMinutes": { - "message": "Dies kann einige Minuten dauern." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Passwörter werden analysiert..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Berichte werden erstellt ..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Fertig!" }, "riskInsightsRunReport": { "message": "Bericht ausführen" @@ -5840,10 +5855,6 @@ "message": "Du kennst das Passwort nicht? Frage den Absender nach dem Passwort, das für dieses Send benötigt wird.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Dieses Send ist standardmäßig ausgeblendet. Du kannst die Sichtbarkeit mit dem Button unten umschalten.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Anhänge herunterladen" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ich akzeptiere diese Risiken und Richtlinien-Aktualisierungen" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persönlichen Tresor entfernen" }, @@ -6429,6 +6455,10 @@ "message": "Sends anzeigen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verifiziere deine E-Mail, um dieses Send anzuzeigen", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Der Bitwarden Benutzer, der dieses Send erstellt hat, hat sich entschieden, seine E-Mail-Adresse zu verstecken. Du solltest sicherstellen, dass du der Quelle dieses Links vertraust, bevor du dessen Inhalt verwendest oder herunterlädst.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6674,7 +6704,7 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 Einladung gesendet" }, "bulkReinviteSentToast": { "message": "$COUNT$ invitations sent", @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, + "invalidEmailOrVerificationCode": { + "message": "E-Mail oder Verifizierungscode ungültig" + }, "keyConnectorDomain": { "message": "Key Connector-Domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archivierte Einträge werden hier angezeigt und von allgemeinen Suchergebnissen und Auto-Ausfüllen-Vorschlägen ausgeschlossen." }, - "itemWasSentToArchive": { - "message": "Eintrag wurde ins Archiv verschoben" + "itemArchiveToast": { + "message": "Eintrag archiviert" }, - "itemWasUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" - }, - "itemUnarchived": { - "message": "Eintrag wird nicht mehr archiviert" + "itemUnarchivedToast": { + "message": "Eintrag nicht mehr archiviert" }, "bulkArchiveItems": { "message": "Einträge archiviert" @@ -12583,7 +12613,7 @@ "message": "Bist du sicher, dass du fortfahren möchtest?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Fehler: Entschlüsselung nicht möglich" }, "userVerificationFailed": { "message": "Benutzerverifizierung fehlgeschlagen." @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Du hast die gesamten $GB$ GB deines verschlüsselten Speichers verwendet. Um mit dem Speichern von Dateien fortzufahren, füge mehr Speicher hinzu." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Wer kann das sehen" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Gib mehrere E-Mail-Adressen ein, indem du sie mit einem Komma trennst." }, + "emailsRequiredChangeAccessType": { + "message": "E-Mail-Verifizierung erfordert mindestens eine E-Mail-Adresse. Ändere den Zugriffstyp oben, um alle E-Mails zu entfernen." + }, "emailPlaceholder": { "message": "benutzer@bitwarden.com, benutzer@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Ungültiges Send-Passwort" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Personen müssen das Passwort eingeben, um dieses Send anzusehen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Beim Aktualisieren deiner Zahlungsmethode ist ein Fehler aufgetreten." + }, + "sendPasswordInvalidAskOwner": { + "message": "Ungültiges Passwort. Frage den Absender nach dem Passwort, das benötigt wird, um auf dieses Send zuzugreifen.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 0aef25ee9cb..8b4bc6fc581 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Η συνδρομή έχει αποκατασταθεί." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να ακυρώσετε; Θα χάσετε την πρόσβαση σε όλες τις λειτουργίες αυτής της συνδρομής στο τέλος αυτού του κύκλου χρέωσης." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Επεξεργασία χρήστη $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Δεν γνωρίζετε τον κωδικό; Ζητήστε από τον αποστολέα τον κωδικό που απαιτείται για την πρόσβαση σε αυτό το Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Αυτό το send είναι κρυμμένο από προεπιλογή. Μπορείτε να αλλάξετε την ορατότητά του χρησιμοποιώντας το παρακάτω κουμπί.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Λήψη συνημμένων" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Προσωπική Ιδιοκτησία" }, @@ -6429,6 +6455,10 @@ "message": "Προβολή Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Ο χρήστης Bitwarden που δημιούργησε αυτό το send έχει επιλέξει να κρύψει τη διεύθυνση email του. Πρέπει να διασφαλίσετε ότι εμπιστεύεστε την πηγή αυτού του συνδέσμου πριν χρησιμοποιήσετε ή κατεβάσετε το περιεχόμενό του.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8991d32f06a..34aa05d5c84 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4337,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4596,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5439,8 +5445,8 @@ "minimumNumberOfWords": { "message": "Minimum number of words" }, - "passwordTypePolicyOverride": { - "message": "Password type", + "overridePasswordTypePolicy": { + "message": "Password Type", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5849,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6145,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6438,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7396,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -10609,6 +10633,15 @@ "huntressEventIntegrationDesc": { "message": "Send event data to your Huntress SIEM instance" }, + "integrationConnectedSuccessfully":{ + "message": "$INTEGRATION$ connected successfully.", + "placeholders": { + "integration": { + "content": "$1", + "example": "Crowdstrike" + } + } + }, "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, @@ -10962,6 +10995,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, + "memberAccessReportLoadError": { + "message": "Failed to load the member access report. This may be due to a large organization size or network issue. Please try again or contact support if the problem persists." + }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, @@ -12862,6 +12898,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12874,6 +12925,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12898,6 +12952,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12953,5 +13019,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 1617bfb4580..4990efea695 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription has expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is cancelled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 8845ef9f042..24414fca4e6 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription has expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is cancelled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analysing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analysing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the Sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personal Ownership" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 1e289a324fe..2d3f8d29a29 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "La abono estis reinstalita." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ĉu vi certas, ke vi volas nuligi? Vi perdos aliron al ĉiuj funkcioj de ĉi tiu abono fine de ĉi tiu faktura ciklo." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redaktiĝis uzanto $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Ĉu vi ne scias la pasvorton? Petu al la Sendinto la pasvorton bezonatan por aliri ĉi tiun Sendon.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ĉi tiu sendado estas kaŝita defaŭlte. Vi povas ŝalti ĝian videblecon per la suba butono.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Persona Posedo" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 2d85309144f..6d4d75e20fc 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "La suscripción ha sido restablecida." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "¿Estás seguro de que quieres cancelar? Perderá el acceso a todas las funciones de esta suscripción al final de este ciclo de facturación." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Usuario $ID$ editado.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "¿No conoce la contraseña? Pídele al remitente la contraseña necesaria para acceder a este enviar.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defecto. Puede cambiar su visibilidad usando el botón de abajo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Descargar archivos adjuntos" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Propiedad personal" }, @@ -6429,6 +6455,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "El usuario Bitwarden que creó este Send ha elegido ocultar su dirección de correo electrónico. Deberías asegurarte de que confías en la fuente de este enlace antes de usar o descargar su contenido.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificación no válido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 60a2690785c..448103ed594 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Tellimus on uuesti aktiveeritud." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Oled kindel, et soovid tellimuse tühistada? Kaotad sellega arveperioodi lõpus kõik tellimisega kaasnevad eelised." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muutis kasutajat $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Sa ei tea parooli? Küsi seda konkreetse Sendi saatjalt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "See Send on vaikeseades peidetud. Saad selle nähtavust alloleva nupu abil seadistada.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lae manused alla" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personaalne salvestamine" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Selle Sendi looja ei soovi oma e-posti aadressi avaldada. Palun veendu, et see pärineb usaldusväärsest allikast, enne kui asud selle sisu kasutama või faile alla laadima.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Vale kinnituskood" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index a75f95994c0..3376ef33f63 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Harpidetza berrezarri da." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ziur zaude ezeztatu nahi duzula? Harpidetza honen ezaugarri guztiak galduko dituzu fakturazio ziklo honen amaieran." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ erabiltzailea editatua.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Ez duzu pasahitza ezagutzen? Eskatu bidaltzaileari Send honetara sartzeko behar den pasahitza.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send hau modu lehenetsian ezkutatuta dago. Beheko botoia sakatuz alda dezakezu ikusgarritasuna.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ezabatu kutxa gotor pertsonala" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Send hau sortu duen Bitwarden erabiltzaileak emaileko helbidea ezkutatzea erabaki du. Ziurtatu lotura honen iturrian konfiantza duzula edukia erabili edo deskargatu aurretik.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 5c046211648..25b2a61c256 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "اشتراک دوباره برقرار شده است." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "آیا مطمئنید که می‌خواهید لغو کنید؟ در پایان این چرخه صورتحساب، دسترسی به همه ویژگی‌های این اشتراک را از دست خواهید داد." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "کاربر $ID$ ویرایش شد.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "کلمه عبور را نمی‌دانید؟ از فرستنده کلمه عبور لازم را برای دسترسی به این ارسال بخواهید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "این ارسال به طور پیش‌فرض پنهان است. با استفاده از دکمه زیر می‌توانید نمایان بودن آن را تغییر دهید.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "بارگیری پیوست‌ها" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "حذف گاوصندوق شخصی" }, @@ -6429,6 +6455,10 @@ "message": "مشاهده ارسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "کاربر Bitwarden که این ارسال را ایجاد کرده است انتخاب کرده که نشانی ایمیل خود را پنهان کند. قبل از استفاده یا دانلود محتوای این پیوند، باید مطمئن شوید که به منبع این پیوند اعتماد دارید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "دامنه رابط کلید" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 7cc931fd9f8..f562a98c60c 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Tilaus palautettiin." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Haluatko varmasti irtisanoa tilauksen? Menetät pääsyn kaikkiin tilauksen tarjoamiin ominaisuuksiin kuluvan laskutuskauden lopussa." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Muokkasi käyttäjää \"$ID$\".", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Etkö tiedä salasanaa? Pyydä lähettäjältä tämän Sendin avaukseen tarvittavaa salasanaa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send on oletusarvoisesti piilotettu. Voit vaihtaa sen näkyvyyttä alla olevalla painikkeella.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lataa liitteet" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Poista yksityinen holvi" }, @@ -6429,6 +6455,10 @@ "message": "Näytä Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Sendin luonut Bitwarden-käyttäjä on piilottanut sähköpostiosoitteensa. Varmista, että linkin lähde on luotettava ennen kuin käytät tai lataat sen sisältöä.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 415793a4b98..af1a0105c83 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Naibalik na ang subscription." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigurado ka bang gusto mong kanselahin? Mawawala ang access mo sa lahat ng tampok ng subscription na ito sa pagtatapos ng siklo ng pagsingil na ito." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Na-edit ang user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Hindi mo ba alam ang password Itanong sa nagpadala ang password na kailangan para ma-access ang Padala na ito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ang Send na ito ay nakatago bilang default. Maaari mong i toggle ang visibility nito gamit ang pindutan sa ibaba.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Alisin ang indibidwal na vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Ang gumagamit ng Bitwarden na lumikha ng Send na ito ay piniling itago ang kanilang email address. Dapat mong tiyakin na pinagkakatiwalaan mo ang pinagmulan ng link na ito bago gamitin o i download ang nilalaman nito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Maling verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index eb9b2d1c915..65a62b5a12d 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Votre abonnement a été rétabli." }, + "resubscribe": { + "message": "Se réabonner" + }, + "yourSubscriptionIsExpired": { + "message": "Votre abonnement a expiré" + }, + "yourSubscriptionIsCanceled": { + "message": "Votre abonnement est annulé" + }, "cancelConfirmation": { "message": "Êtes-vous sûr de vouloir annuler ? Vous perdrez l’accès à toutes les fonctionnalités de l’abonnement à la fin de ce cycle de facturation." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilisateur $ID$ automatiquement confirmé.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilisateur $ID$ modifié.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Génération de votre Intelligence d'Accès..." }, - "fetchingMemberData": { - "message": "Récupération des données des membres..." - }, - "analyzingPasswordHealth": { - "message": "Analyse de la santé du mot de passe..." - }, - "calculatingRiskScores": { - "message": "Calcul des niveaux de risque..." - }, - "generatingReportData": { - "message": "Génération des données du rapport..." - }, - "savingReport": { - "message": "Enregistrement du rapport..." - }, - "compilingInsights": { - "message": "Compilation des aperçus..." - }, "loadingProgress": { "message": "Chargement de la progression" }, - "thisMightTakeFewMinutes": { - "message": "Cela peut prendre quelques minutes." + "reviewingMemberData": { + "message": "Révision des données du membre..." + }, + "analyzingPasswords": { + "message": "Analyse des mots de passe..." + }, + "calculatingRisks": { + "message": "Calcul des risques..." + }, + "generatingReports": { + "message": "Génération du rapport..." + }, + "compilingInsightsProgress": { + "message": "Compilation des observations..." + }, + "reportGenerationDone": { + "message": "Fini !" }, "riskInsightsRunReport": { "message": "Exécuter le rapport" @@ -5840,10 +5855,6 @@ "message": "Vous ne connaissez pas le mot de passe ? Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ce Send est masqué par défaut. Vous pouvez changer sa visibilité en utilisant le bouton ci-dessous.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Télécharger les pièces jointes" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "J'accepte ces risques et mises à jour de la politique de sécurité" }, + "autoConfirmEnabledByAdmin": { + "message": "Activé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByAdmin": { + "message": "Désactivé le paramètre de confirmation automatique de l'utilisateur" + }, + "autoConfirmEnabledByPortal": { + "message": "Ajout de la politique de sécurité de confirmation automatique de l'utilisateur" + }, + "autoConfirmDisabledByPortal": { + "message": "Politique de sécurité de confirmation automatique de l'utilisateur retirée" + }, + "system": { + "message": "Système" + }, "personalOwnership": { "message": "Supprimer le coffre individuel" }, @@ -6429,6 +6455,10 @@ "message": "Voir le Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Vérifiez votre courriel pour afficher ce Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'utilisateur de Bitwarden qui a créé ce Send a choisi de masquer son adresse électronique. Vous devriez vous assurer que vous faites confiance à la source de ce lien avant d'utiliser ou de télécharger son contenu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Code de vérification invalide" }, + "invalidEmailOrVerificationCode": { + "message": "Courriel ou code de vérification invalide" + }, "keyConnectorDomain": { "message": "Domaine Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Les éléments archivés apparaîtront ici et seront exclus des résultats de recherche généraux et des suggestions de remplissage automatique." }, - "itemWasSentToArchive": { - "message": "L'élément a été envoyé à l'archive" + "itemArchiveToast": { + "message": "Élément archivé" }, - "itemWasUnarchived": { - "message": "L'élément a été désarchivé" - }, - "itemUnarchived": { - "message": "L'élément a été désarchivé" + "itemUnarchivedToast": { + "message": "Élément désarchivé" }, "bulkArchiveItems": { "message": "Éléments archivés" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Vous avez utilisé tous les $GB$ Go de votre stockage chiffré. Pour continuer à stocker des fichiers, ajoutez plus de stockage." }, + "extensionPromptHeading": { + "message": "Obtenir l'extension pour un accès facile au coffre" + }, + "extensionPromptBody": { + "message": "Avec l'extension de navigateur installée, vous emmènerez Bitwarden partout en ligne. Il remplira les mots de passe, faisant en sorte que vous puissiez vous connecter à vos comptes en un seul clic." + }, + "extensionPromptImageAlt": { + "message": "Un navigateur web montrant l'extension Bitwarden avec des éléments de saisie automatique pour la page web actuelle." + }, + "skip": { + "message": "Ignorer" + }, + "downloadExtension": { + "message": "Télécharger l'extension" + }, "whoCanView": { "message": "Qui peut afficher" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Entrez plusieurs courriels en les séparant avec une virgule." }, + "emailsRequiredChangeAccessType": { + "message": "La vérification de courriel requiert au moins une adresse courriel. Pour retirer tous les courriels, changez le type d'accès ci-dessus." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Mot de passe Send invalide" }, + "vaultWelcomeDialogTitle": { + "message": "Vous y êtes! Bienvenue sur Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Enregistrez tous vos mots de passe et vos informations personnelles dans votre coffre de Bitwarden. Nous vous ferons faire la visite." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Commencer la visite" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Ignorer" + }, "sendPasswordHelperText": { "message": "Les personnes devront entrer le mot de passe pour afficher ce Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Une erreur s'est produite lors de la mise à jour de votre mode de paiement." + }, + "sendPasswordInvalidAskOwner": { + "message": "Mot de passe invalide. Demandez à l'expéditeur le mot de passe nécessaire pour accéder à ce Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Ce Send expire à $TIME$ le $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 8357f5d0747..17426524033 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index b075b91a860..7685aaae5a1 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "המנוי הופעל מחדש." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "האם אתה בטוח שברצונך לבטל? ביטול המנוי יגרום לאיבוד כל האפשרויות השמורות למנויים בסיום מחזור החיוב הנוכחי." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "משתמש שנערך $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "הרץ דוח" @@ -5840,10 +5855,6 @@ "message": "לא יודע את הסיסמה? בקש מהשולח את הסיסמה הדרושה עבור סֵנְד זה.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "סֵנְד זה מוסתר כברירת מחדל. אתה יכול לשנות את מצב הנראות שלו באמצעות הלחצן למטה.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "הורד צרופות" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "אני מסכים לסיכונים ועדכוני מדיניות אלה" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "הסר כספת אישית" }, @@ -6429,6 +6455,10 @@ "message": "הצג סֵנְד", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "משתמש ה־Bitwarden שיצר את סֵנְד זה בחר להסתיר את כתובת הדוא\"ל שלו. עליך לוודא שאתה בוטח במקור של קישור זה לפני שימוש או הורדה של התוכן שלו.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "קוד אימות שגוי" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "דומיין של Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "פריטים בארכיון יופיעו כאן ויוחרגו מתוצאות חיפוש כללי והצעות למילוי אוטומטי." }, - "itemWasSentToArchive": { - "message": "הפריט נשלח לארכיון" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "הפריט הוסר מהארכיון" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "הפריטים הועברו לארכיון" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 4e261d672bb..1fad1304650 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index ced4fc03b1c..c67344da799 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Pretplata je vraćena" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigurno želiš otkazati? Izgubiti ćeš pristup svim ovim pretplatnim značajkama kad istekne rok naplate." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Uređen korisnik $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generiranje tvoje pristupne inteligencije..." }, - "fetchingMemberData": { - "message": "Dohvaćanje podataka o članu…" - }, - "analyzingPasswordHealth": { - "message": "Analiziranje zdravlja lozinke…" - }, - "calculatingRiskScores": { - "message": "Izračun ocjene rizika…" - }, - "generatingReportData": { - "message": "Generiranje izvješća…" - }, - "savingReport": { - "message": "Spremanje izvještaja…" - }, - "compilingInsights": { - "message": "Sastavljanje uvida…" - }, "loadingProgress": { "message": "Učitavanje napretka" }, - "thisMightTakeFewMinutes": { - "message": "Ovo može potrajati nekoliko minuta." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Pokreni izvješće" @@ -5840,10 +5855,6 @@ "message": "Ne znaš lozinku? Upitaj pošiljatelja za lozinku za pristup ovom Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ovaj je Send zadano skriven. Moguće mu je promijeniti vidljivost.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Preuzmi privitke" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Prihavaćam ove rizike i ažurirana pravila" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Ukloni osobni trezor" }, @@ -6429,6 +6455,10 @@ "message": "Pogledaj Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden korisnik koji je stvorio ovaj Send odabrao/la je sakriti svoju e-poštu. Koristi i/ili preuzmi ove podatke samo ako vjeruješ izvoru iz kojeg je primljena ova vezu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena konektora ključa" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, - "itemWasSentToArchive": { - "message": "Stavka poslana u arhivu" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Stavka vraćena iz arhive" - }, - "itemUnarchived": { - "message": "Stavka vraćena iz arhive" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Stavke arhivirane" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index f4c9144cc6d..ed38bd4b0e7 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Az előfizetés visszaállításra került." }, + "resubscribe": { + "message": "Feliratkozás ismét" + }, + "yourSubscriptionIsExpired": { + "message": "Az előfizetés lejárt." + }, + "yourSubscriptionIsCanceled": { + "message": "Az előfizetés törlésre került." + }, "cancelConfirmation": { "message": "Biztosan törlésre kerüljön? A számlázási időszak végén az összes előfizetési hozzáférés elveszik." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "$ID$ felhasználó automatikusan megerősítésre került.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ azonosítójú felhasználó módosításra került.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Hozzáférési intelligencia generálása..." }, - "fetchingMemberData": { - "message": "Tagi adatok lekérése..." - }, - "analyzingPasswordHealth": { - "message": "A jelszó állapot elemzése..." - }, - "calculatingRiskScores": { - "message": "Kockázati pontszámok kiszámítása..." - }, - "generatingReportData": { - "message": "Jelentés adatok generálása..." - }, - "savingReport": { - "message": "Jelentés mentése..." - }, - "compilingInsights": { - "message": "Betekintések összeállítása..." - }, "loadingProgress": { "message": "Feldolgozás betöltése" }, - "thisMightTakeFewMinutes": { - "message": "Ez eltarthat pár percig." + "reviewingMemberData": { + "message": "Tagi adatok lekérése..." + }, + "analyzingPasswords": { + "message": "A jelszavak elemzése..." + }, + "calculatingRisks": { + "message": "A kockázatok kiszámítása..." + }, + "generatingReports": { + "message": "Jelentések generálása..." + }, + "compilingInsightsProgress": { + "message": "Betekintések összeállítása..." + }, + "reportGenerationDone": { + "message": "Kész!" }, "riskInsightsRunReport": { "message": "Jelentés futtatása" @@ -5840,10 +5855,6 @@ "message": "Nem ismerjük a jelszót? Kérdezzünk rá a küldőnél a Send elérésére szükséges jelszóért.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ez a Send alapértelmezésben rejtett. Az alábbi gombbal átváltható a láthatósága.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Mellékletek letöltése" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Elfogadom ezeket a kockázatokat és a szabályzat frissítéseit." }, + "autoConfirmEnabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás bekapcsolásra került." + }, + "autoConfirmDisabledByAdmin": { + "message": "Az automatikus felhasználó megerősítés beállítás kikapcsolásra került." + }, + "autoConfirmEnabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály hozzáadásra került." + }, + "autoConfirmDisabledByPortal": { + "message": "Az automatikus felhasználó megerősítés rendszabály eltávolításra került." + }, + "system": { + "message": "Rendszer" + }, "personalOwnership": { "message": "Személyes tulajdon" }, @@ -6429,6 +6455,10 @@ "message": "Send megtekintése", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Ellenőrizzük az email címet a Send elem megtekintéséhez.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Az ezt a Send elemet létrehozó Bitwarden felhasználó úgy döntött, hogy elrejti email címét. Mielőtt felhasználnánk vagy letöltenénk a tartalmát, ellenőrizzük a hivatkozás megbízhatóságát.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, + "invalidEmailOrVerificationCode": { + "message": "Az email cím vagy az ellenőrző kód érvénytelen." + }, "keyConnectorDomain": { "message": "Key Connector tartomány" }, @@ -10906,7 +10939,7 @@ "message": "A biztonsági kockázatok azonosítása a tagok hozzáférésének ellenőrzésével" }, "onlyAvailableForEnterpriseOrganization": { - "message": "Vállalati tervre frissítéssel gyorsan megtekinthető a tagok hozzáférése a szervezetben." + "message": "Vállalati csomagra frissítéssel gyorsan megtekinthető a tagok hozzáférése a szervezetben." }, "date": { "message": "Dátum" @@ -11896,20 +11929,17 @@ "noItemsInArchiveDesc": { "message": "Az archivált elemek itt jelennek meg és kizárásra kerülnek az általános keresési eredményekből és az automatikus kitöltési javaslatokból." }, - "itemWasSentToArchive": { - "message": "Az elem az archivumba került." + "itemArchiveToast": { + "message": "Az elem archiválásra került." }, - "itemWasUnarchived": { + "itemUnarchivedToast": { "message": "Az elem visszavételre került az archivumból." }, - "itemUnarchived": { - "message": "Az elemek visszavéelre kerültek az archivumból." - }, "bulkArchiveItems": { "message": "Az elemek archiválásra kerültek." }, "bulkUnarchiveItems": { - "message": "Az elemek visszavéelre kerültek az archivumból." + "message": "Az elemek visszavételre kerültek az archivumból." }, "archiveItem": { "message": "Elem archiválása", @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "A titkosított tárhely összes $GB$ mérete felhasználásra került. A fájlok tárolásának folytatásához adjunk hozzá további tárhelyet." }, + "extensionPromptHeading": { + "message": "Szerezzük be a kiterjesztést a széf könnyű eléréséhez." + }, + "extensionPromptBody": { + "message": "A böngésző kiterjesztés telepítésével a Bitwardent mindenhová magunkkal vihetjük. Kitölti a jelszavakat, így egyetlen kattintással bejelentkezhetünk a fiókjainkba." + }, + "extensionPromptImageAlt": { + "message": "Egy webböngésző, amely a Bitwarden kiterjesztést jeleníti meg az aktuális weboldal automatikus kitöltési elemeivel." + }, + "skip": { + "message": "Kihagyás" + }, + "downloadExtension": { + "message": "Kiterjesztés letöltése" + }, "whoCanView": { "message": "Ki láthatja" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Írjunk be több email címet vesszővel elválasztva." }, + "emailsRequiredChangeAccessType": { + "message": "Az email cím ellenőrzéshez legalább egy email cím szükséges. Az összes email cím eltávolításához módosítsuk a fenti hozzáférési típust." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Érvénytelen a Send jelszó." }, + "vaultWelcomeDialogTitle": { + "message": "Megérkeztünk! Üdvözlet a Bitwardenben" + }, + "vaultWelcomeDialogDescription": { + "message": "Az összes jelszó és személyes adat tárolása a Bitwarden trezorban. Nézzünk körbe." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Túra elkezdése" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Kihagyás" + }, "sendPasswordHelperText": { "message": "A személyeknek meg kell adniuk a jelszót a Send elem megtekintéséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Hiba történt a fizetési mód frissítésekor." + }, + "sendPasswordInvalidAskOwner": { + "message": "A jelszó érvénytelen. Kérjük el a feladótól a Send elem eléréséhez szükséges jelszót.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "A Send elem lejár: $DATE$ - $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 270b8624c24..c1967096299 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Langganan telah dipulihkan." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Anda yakin ingin membatalkan? Anda akan kehilangan akses ke semua fitur langganan ini di akhir siklus penagihan ini." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ telah diedit.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Jalankan laporan" @@ -5840,10 +5855,6 @@ "message": "Tidak tahu sandinya? Tanyakan pengirim untuk sandi yang diperlukan untuk mengakses Kirim ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Pengiriman ini disembunyikan secara default. Anda dapat mengubah visibilitasnya menggunakan tombol di bawah ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Unduh lampiran" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Saya menerima risiko dan pembaruan kebijakan ini" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Kepemilikan Pribadi" }, @@ -6429,6 +6455,10 @@ "message": "Lihat Kirim", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Pengguna Bitwarden yang membuat Send ini memilih untuk menyembunyikan alamat emailnya. Kamu harus yakin bahwa kamu mempercayai sumber dari link it sebelum mengunduh isinya.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index c2fb3effdf0..ac04390751d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "L'abbonamento è stato ripristinato." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sei sicuro di voler annullare il tuo abbonamento? Alla fine di questo ciclo di fatturazione perderai l'accesso a tutte le funzionalità aggiuntive date dall'abbonamento." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utente $ID$ modificato.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generazione del tuo Access Intelligence..." }, - "fetchingMemberData": { - "message": "Recupero dei dati dei membri..." - }, - "analyzingPasswordHealth": { - "message": "Analisi della salute della password..." - }, - "calculatingRiskScores": { - "message": "Calcolo dei punteggi di rischio..." - }, - "generatingReportData": { - "message": "Generazione dati del rapporto..." - }, - "savingReport": { - "message": "Salvataggio..." - }, - "compilingInsights": { - "message": "Compilazione dei dati..." - }, "loadingProgress": { "message": "Caricamento in corso" }, - "thisMightTakeFewMinutes": { - "message": "Attendi..." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Avvia report" @@ -5840,10 +5855,6 @@ "message": "Non conosci la password? Chiedi al mittente la password necessaria per accesso a questo Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Questo Send è nascosto per impostazione predefinita. Modifica la sua visibilità usando questo pulsante.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Scarica allegati" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Accetto questi rischi e aggiornamenti sulle politiche" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Rimuovi cassaforte individuale" }, @@ -6429,6 +6455,10 @@ "message": "Visualizza Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "L'utente Bitwarden che ha creato questo Send ha nascosto il suo indirizzo email. Devi essere sicuro di fidarti della fonte di questo link prima di usare o scaricare il suo contenuto.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Codice di verifica non valido" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Dominio Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Gli elementi archiviati appariranno qui e saranno esclusi dai risultati di ricerca e dall'auto-riempimento." }, - "itemWasSentToArchive": { - "message": "Elemento archiviato" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Elemento estratto dall'archivio" - }, - "itemUnarchived": { - "message": "Elemento estratto dall'archivio" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Elementi archiviati" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Hai usato tutti i $GB$ GB del tuo spazio di archiviazione crittografato. Per archiviare altri file, aggiungi altro spazio." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Chi può visualizzare" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Inserisci più indirizzi email separandoli con virgole." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Password del Send non valida" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index cd78e0a269a..b3b5a975ec0 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "契約が再開されました。" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "本当にキャンセルしますか?契約していたすべての追加機能が請求期間の終期で利用できなくなります。" }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ユーザー「$ID$」の編集", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "パスワードがわかりませんか?このSendにアクセスするには送信者にパスワードをご確認ください。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "このSendはデフォルトでは非表示になっています。下のボタンで表示・非表示が切り替え可能です。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "添付ファイルをダウンロード" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "個別の保管庫を削除" }, @@ -6429,6 +6455,10 @@ "message": "Send を表示", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "この Send を作成した Bitwarden ユーザーが、自身のメールアドレスを非表示にしました。 コンテンツを使用またはダウンロードする前に、このリンクのソースが信頼できるかどうか確認する必要があります。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "認証コードが間違っています" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6de5179793d..39a28d2a0c7 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index b95256dfacd..4e6a8265ad6 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 7a378184a39..85e79eba91d 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "ಚಂದಾದಾರಿಕೆಯನ್ನು ಪುನಃ ಸ್ಥಾಪಿಸಲಾಗಿದೆ." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "ನೀವು ರದ್ದು ಮಾಡಲು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? ಈ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರದ ಕೊನೆಯಲ್ಲಿ ಈ ಎಲ್ಲಾ ಚಂದಾದಾರಿಕೆಯ ವೈಶಿಷ್ಟ್ಯಗಳಿಗೆ ನೀವು ಪ್ರವೇಶವನ್ನು ಕಳೆದುಕೊಳ್ಳುತ್ತೀರಿ." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "ತಿದ್ಸಿದ ಬಳಕೆದಾರ $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "ಪಾಸ್ವರ್ಡ್ ತಿಳಿದಿಲ್ಲವೇ? ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಅಗತ್ಯವಿರುವ ಪಾಸ್‌ವರ್ಡ್‌ಗಾಗಿ ಕಳುಹಿಸುವವರನ್ನು ಕೇಳಿ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಮರೆಮಾಡಲಾಗಿದೆ. ಕೆಳಗಿನ ಬಟನ್ ಬಳಸಿ ನೀವು ಅದರ ಗೋಚರತೆಯನ್ನು ಟಾಗಲ್ ಮಾಡಬಹುದು.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "ವೈಯಕ್ತಿಕ ಮಾಲೀಕತ್ವ" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ರಚಿಸಿದ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಬಳಕೆದಾರರು ತಮ್ಮ ಇಮೇಲ್ ವಿಳಾಸವನ್ನು ಮರೆಮಾಡಲು ಆಯ್ಕೆ ಮಾಡಿದ್ದಾರೆ. ಈ ಲಿಂಕ್‌ನ ವಿಷಯವನ್ನು ಬಳಸುವ ಅಥವಾ ಡೌನ್‌ಲೋಡ್ ಮಾಡುವ ಮೊದಲು ಅದರ ಮೂಲವನ್ನು ನೀವು ನಂಬಿದ್ದೀರಿ ಎಂದು ನೀವು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಬೇಕು.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index ad9b80b9f6a..273bdddadab 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "구독을 복원했습니다." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "정말로 취소하시겠습니까? 청구 주기 후에 이 구독의 모든 기능에 대한 접근을 잃게 됩니다." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "$ID$ 사용자를 편집했습니다.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "비밀번호를 모르시나요? 보낸 사람에게 Send에 접근할 수 있는 비밀번호를 요청하세요.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "이 Send는 기본적으로 숨겨져 있습니다. 아래의 버튼을 눌러 공개 여부를 전환할 수 있습니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "첨부 파일 다운로드" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "개인 소유권" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "이 Send를 생성한 Bitwarden 사용자가 자신의 이메일 주소를 숨겼습니다. 이 링크에 접속하거나 내용을 다운로드하기 전에, 이 링크의 출처를 신뢰하는지 확인하십시오.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 4875dc4fe31..26f960a29d7 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonements tika atjaunots." }, + "resubscribe": { + "message": "Atsākt abonēšanu" + }, + "yourSubscriptionIsExpired": { + "message": "Abonements ir beidzies" + }, + "yourSubscriptionIsCanceled": { + "message": "Abonements ir atcelts" + }, "cancelConfirmation": { "message": "Vai tiešām atcelt? Tiks zaudēta piekļuve visām abonementa iespējām pēc pašreizējā norēķinu laika posma beigām." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Labots lietotājs $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Izveido informāciju par Tavu piekļuvi…" }, - "fetchingMemberData": { - "message": "Iegūst dalībnieku datus…" - }, - "analyzingPasswordHealth": { - "message": "Izvērtē paroļu veselību…" - }, - "calculatingRiskScores": { - "message": "Aprēķina risku novērtējumu…" - }, - "generatingReportData": { - "message": "Izveido atskaites datus…" - }, - "savingReport": { - "message": "Saglabā atskaiti…" - }, - "compilingInsights": { - "message": "Apkopo ieskatus…" - }, "loadingProgress": { "message": "Ielādē virzību" }, - "thisMightTakeFewMinutes": { - "message": "Tas var aizņemt dažas minūtes." + "reviewingMemberData": { + "message": "Pārskata dalībnieku datus…" + }, + "analyzingPasswords": { + "message": "Izvērtē paroles…" + }, + "calculatingRisks": { + "message": "Aprēķina riskus…" + }, + "generatingReports": { + "message": "Izveido pārskatus…" + }, + "compilingInsightsProgress": { + "message": "Apkopo ieskatus…" + }, + "reportGenerationDone": { + "message": "Gatavs." }, "riskInsightsRunReport": { "message": "Izveidot atskaiti" @@ -5840,10 +5855,6 @@ "message": "Nezini paroli? Vaicā sūtītājam paroli, kas ir nepieciešama, lai piekļūtu šim Send!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Šis Send ir paslēpts pēc noklusējuma. Tā redzamību var pārslēgt ar zemāk esošo pogu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Lejupielādēt pielikumus" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Es pieņemu šos riskus un pamatnostādnes atjauninājumus" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personīgās īpašumtiesības" }, @@ -6429,6 +6455,10 @@ "message": "Apskatīt Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Jāapliecina sava e-pasta adrese, lai apskatītu šo Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden lietotājs, kurš izveidoja šo Send, ir izvēlējies slēpt savu e-pasta adresi. Ir jāpārliecinās par šīs saites avota uzticamību, pirms saturs tiek izmantots vai lejupielādēts.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Nederīgs apliecinājuma kods" }, + "invalidEmailOrVerificationCode": { + "message": "Nederīga e-pasta adrese vai apliecinājuma kods" + }, "keyConnectorDomain": { "message": "Key Connector domēns" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Šeit parādīsies arhivētie vienumi, un tie netiks iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos." }, - "itemWasSentToArchive": { - "message": "Vienums tika ievietots arhīvā" + "itemArchiveToast": { + "message": "Vienums ievietots arhīvā" }, - "itemWasUnarchived": { - "message": "Vienums tika izņemts no arhīva" - }, - "itemUnarchived": { - "message": "Vienums tika izņemts no arhīva" + "itemUnarchivedToast": { + "message": "Vienums izņemts no arhīva" }, "bulkArchiveItems": { "message": "Vienumi tika arhivēti" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "E-pasta apliecināšanai ir nepieciešama vismaz viena e-pasta adrese. Lai noņemtu visas e-pasta adreses, augstāk jānomaina piekļūšanas veids." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Cilvēkiem būs jāievada parole, lai apskatītu šo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Nederīga parole. Jāvaicā nepieciešamā parole nosūtītājam, lai piekļūtu šim Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Šī Send derīgums beigsies $DATE$ plkst. $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index f0173da95e5..c700f329c65 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "പാസ്‌വേഡ് അറിയില്ലേ? ഈ അയയ്‌ക്കൽ ആക്‌സസ് ചെയ്യുന്നതിന് ആവശ്യമായ പാസ്‌വേഡിനായി അയച്ചയാളോട് ചോദിക്കുക.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "ഈ Send സ്ഥിരസ്ഥിതിയായി മറച്ചിരിക്കുന്നു. ചുവടെയുള്ള ബട്ടൺ ഉപയോഗിച്ചാൽ നിങ്ങൾക്ക് അതിന്റെ ദൃശ്യപരത ടോഗിൾ ചെയ്യാൻ കഴിയും.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "വ്യക്തിഗത ഉടമസ്ഥാവകാശം" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 35031280ec3..1fc1ef73f4b 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index b95256dfacd..4e6a8265ad6 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 07db562d56b..6c463a61e64 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnementet har blitt gjeninnført." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Er du sikker på at du vil avbryte? Du vil miste tilgang til alle funksjonene til dette abonnementet etter den inneværende regningsperioden." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerte brukeren $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Vet du ikke passordet? Be avsender om nødvendig tilgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denne Send-en er skjult som standard. Du kan veksle synlighet ved å bruke knappen nedenfor.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Personlig eierskap" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-brukeren som opprettet denne sendingen, har valgt å skjule deres e-postadresse. Du bør forsikre deg om at du stoler på kilden til denne lenken før du bruker eller laster ned innholdet.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index cd5e07cc33e..9ef82cc799c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 6754c7a0c51..e50966e3ca1 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Het abonnement is opnieuw geactiveerd." }, + "resubscribe": { + "message": "Opnieuw abonneren" + }, + "yourSubscriptionIsExpired": { + "message": "Je abonnement is verlopen" + }, + "yourSubscriptionIsCanceled": { + "message": "Je abonnement is geannuleerd" + }, "cancelConfirmation": { "message": "Weet je zeker dat je wilt opzeggen? Je verliest toegang tot alle functionaliteiten van dit abonnement aan het einde van deze betalingscyclus." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatisch bevestigde gebruiker $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Gebruiker gewijzigd $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Je toegangsinlichtingen genereren..." }, - "fetchingMemberData": { - "message": "Ledengegevens ophalen..." - }, - "analyzingPasswordHealth": { - "message": "Wachtwoordgezondheid analyseren..." - }, - "calculatingRiskScores": { - "message": "Risicoscores berekenen..." - }, - "generatingReportData": { - "message": "Rapportgegevens genereren..." - }, - "savingReport": { - "message": "Rapport opslaan..." - }, - "compilingInsights": { - "message": "Inzichten compileren..." - }, "loadingProgress": { "message": "Voortgang laden" }, - "thisMightTakeFewMinutes": { - "message": "Dit kan een paar minuten duren." + "reviewingMemberData": { + "message": "Ledengegevens controleren..." + }, + "analyzingPasswords": { + "message": "Wachtwoorden analyseren..." + }, + "calculatingRisks": { + "message": "Risicoscores berekenen..." + }, + "generatingReports": { + "message": "Rapporteren genereren..." + }, + "compilingInsightsProgress": { + "message": "Inzichten compileren..." + }, + "reportGenerationDone": { + "message": "Klaar!" }, "riskInsightsRunReport": { "message": "Rapport uitvoeren" @@ -5840,10 +5855,6 @@ "message": "Weet je het wachtwoord niet? Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Deze Send is standaard verborgen. Je kunt de zichtbaarheid ervan in- en uitschakelen met de knop hieronder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Bijlagen downloaden" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Ik accepteer deze risico's en beleidsupdates" }, + "autoConfirmEnabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling ingeschakeld" + }, + "autoConfirmDisabledByAdmin": { + "message": "Automatisch gebruikersbevestigingsinstelling uitgeschakeld" + }, + "autoConfirmEnabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid toegevoegd" + }, + "autoConfirmDisabledByPortal": { + "message": "Automatisch gebruikersbevestigingsbeleid verwijderd" + }, + "system": { + "message": "Systeem" + }, "personalOwnership": { "message": "Persoonlijk eigendom" }, @@ -6429,6 +6455,10 @@ "message": "Send weergeven", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verifieer je e-mailadres om deze Send te bekijken", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "De Bitwarden-gebruiker die deze Send heeft gemaakt heeft ervoor gekozen het e-mailadres te verbergen. Je moet je ervan verzekeren dat je de bron van deze link vertrouwt voordat je de inhoud gebruikt of downloadt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, + "invalidEmailOrVerificationCode": { + "message": "Ongeldig e-mailadres verificatiecode" + }, "keyConnectorDomain": { "message": "Key Connector-domein" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Gearchiveerde items verschijnen hier en worden uitgesloten van algemene zoekresultaten en automatisch invulsuggesties." }, - "itemWasSentToArchive": { - "message": "Item naar archief verzonden" + "itemArchiveToast": { + "message": "Item gearchiveerd" }, - "itemWasUnarchived": { - "message": "Item uit het archief gehaald" - }, - "itemUnarchived": { - "message": "Item uit het archief gehaald" + "itemUnarchivedToast": { + "message": "Item gedearchiveerd" }, "bulkArchiveItems": { "message": "Items gearchiveerd" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Je hebt alle $GB$ GB aan versleutelde opslag gebruikt. Voeg meer opslagruimte toe om door te gaan met het opslaan van bestanden." }, + "extensionPromptHeading": { + "message": "Gebruik de extensie voor eenvoudige toegang tot je kluis" + }, + "extensionPromptBody": { + "message": "Met de browserextensie kun je Bitwarden overal online gebruiken. Het invullen van wachtwoorden, zodat je met één klik op je accounts kunt inloggen." + }, + "extensionPromptImageAlt": { + "message": "Een webbrowser die de Bitwarden-extensie toont met automatisch invullen voor de huidige webpagina." + }, + "skip": { + "message": "Overslaan" + }, + "downloadExtension": { + "message": "Extensie downloaden" + }, "whoCanView": { "message": "Wie kan weergeven" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Voer meerdere e-mailadressen in door te scheiden met een komma." }, + "emailsRequiredChangeAccessType": { + "message": "E-mailverificatie vereist ten minste één e-mailadres. Om alle e-mailadressen te verwijderen, moet je het toegangstype hierboven wijzigen." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Ongeldig Send-wachtwoord" }, + "vaultWelcomeDialogTitle": { + "message": "Je bent erbij! Welkom bij Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Sla al je wachtwoorden en persoonlijke informatie op in je Bitwarden-kluis. We laten je zien hoe het werkt." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Rondleiding starten" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Overslaan" + }, "sendPasswordHelperText": { "message": "Individuen moeten het wachtwoord invoeren om deze Send te bekijken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Er is een fout opgetreden bij het bijwerken van je betaalmethode." + }, + "sendPasswordInvalidAskOwner": { + "message": "Onjuist wachtwoord. Vraag de afzender om het wachtwoord om toegang te krijgen tot deze Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Deze Send verloopt om $TIME$ op $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 7987b4077d1..f87cf97aa65 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index b95256dfacd..4e6a8265ad6 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 9736337ee0c..82f18f25c31 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Subskrypcja została przywrócona." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Czy na pewno chcesz anulować? Dostęp do wszystkich funkcji związanych z tą subskrypcją zostanie wyłączony na koniec tego okresu rozliczeniowego." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Użytkownik $ID$ został zaktualizowany.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Nie znasz hasła? Poproś nadawcę o hasło, aby uzyskać dostęp do wysyłki.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ta wysyłka jest domyślnie ukryta. Możesz zmienić jej widoczność za pomocą przycisku.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Pobierz załączniki" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Własność osobista" }, @@ -6429,6 +6455,10 @@ "message": "Zobacz Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Użytkownik Bitwarden, który utworzył wysyłkę, zdecydował ukryć swój adres e-mail. Przed użyciem lub pobraniem treści wysyłki upewnij się, że ufasz źródłu tego linku.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Domena Key Connector'a" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index f29637803f0..73b3994fab3 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -15,7 +15,7 @@ "message": "Nenhum aplicativo crítico em risco" }, "critical": { - "message": "Critical ($COUNT$)", + "message": "Críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -24,7 +24,7 @@ } }, "notCritical": { - "message": "Not critical ($COUNT$)", + "message": "Não críticos ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -33,13 +33,13 @@ } }, "criticalBadge": { - "message": "Critical" + "message": "Crítico" }, "accessIntelligence": { "message": "Inteligência de acesso" }, "noApplicationsMatchTheseFilters": { - "message": "No applications match these filters" + "message": "Nenhum aplicativo corresponde aos filtros" }, "passwordRisk": { "message": "Risco de senhas" @@ -48,7 +48,7 @@ "message": "Você não tem permissão para editar este item" }, "reviewAccessIntelligence": { - "message": "Review security reports to find and fix credential risks before they escalate." + "message": "Revise os relatórios de segurança para encontrar e corrigir riscos antes que cresçam." }, "reviewAtRiskLoginsPrompt": { "message": "Revisar credenciais em risco" @@ -269,7 +269,7 @@ } }, "numCriticalApplicationsMarkedSuccess": { - "message": "$COUNT$ applications marked critical", + "message": "$COUNT$ aplicativos marcados como críticos", "placeholders": { "count": { "content": "$1", @@ -278,7 +278,7 @@ } }, "numApplicationsUnmarkedCriticalSuccess": { - "message": "$COUNT$ applications marked not critical", + "message": "$COUNT$ aplicativos marcados como não críticos", "placeholders": { "count": { "content": "$1", @@ -287,7 +287,7 @@ } }, "markAppCountAsCritical": { - "message": "Mark $COUNT$ as critical", + "message": "Marcar $COUNT$ como críticos", "placeholders": { "count": { "content": "$1", @@ -296,7 +296,7 @@ } }, "markAppCountAsNotCritical": { - "message": "Mark $COUNT$ as not critical", + "message": "Marcar $COUNT$ como não críticos", "placeholders": { "count": { "content": "$1", @@ -311,7 +311,7 @@ "message": "Aplicativo" }, "applications": { - "message": "Applications" + "message": "Aplicativos" }, "atRiskPasswords": { "message": "Senhas em risco" @@ -650,7 +650,7 @@ "message": "E-mail" }, "emails": { - "message": "Emails" + "message": "E-mails" }, "phone": { "message": "Telefone" @@ -1284,7 +1284,7 @@ "message": "Selecionar tudo" }, "deselectAll": { - "message": "Deselect all" + "message": "Desselecionar tudo" }, "unselectAll": { "message": "Deselecionar tudo" @@ -1435,10 +1435,10 @@ "message": "Não" }, "noAuth": { - "message": "Anyone with the link" + "message": "Qualquer pessoa com o link" }, "anyOneWithPassword": { - "message": "Anyone with a password set by you" + "message": "Qualquer pessoa com uma senha configurada por você" }, "location": { "message": "Localização" @@ -3338,6 +3338,15 @@ "reinstated": { "message": "A assinatura foi restabelecida." }, + "resubscribe": { + "message": "Reinscrever-se" + }, + "yourSubscriptionIsExpired": { + "message": "Sua assinatura expirou" + }, + "yourSubscriptionIsCanceled": { + "message": "Sua assinatura foi cancelada" + }, "cancelConfirmation": { "message": "Você tem certeza que deseja cancelar? Você perderá o acesso a todos os recursos dessa assinatura no final deste ciclo de faturamento." }, @@ -3357,7 +3366,7 @@ "message": "Próxima cobrança" }, "nextChargeDate": { - "message": "Next charge date" + "message": "Próxima cobrança" }, "plan": { "message": "Plano" @@ -3848,7 +3857,7 @@ "message": "Editar conjunto" }, "viewCollection": { - "message": "View collection" + "message": "Ver conjunto" }, "collectionInfo": { "message": "Informações do conjunto" @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Usuário $ID$ foi confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Editou o usuário $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Gerando sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "Buscando dados de membros..." - }, - "analyzingPasswordHealth": { - "message": "Analisando saúde das senhas..." - }, - "calculatingRiskScores": { - "message": "Calculando pontuações de risco..." - }, - "generatingReportData": { - "message": "Gerando dados do relatório..." - }, - "savingReport": { - "message": "Salvando relatório..." - }, - "compilingInsights": { - "message": "Compilando conhecimentos..." - }, "loadingProgress": { "message": "Progresso de carregamento" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode levar alguns minutos." + "reviewingMemberData": { + "message": "Revisando os dados dos membros..." + }, + "analyzingPasswords": { + "message": "Analisando as senhas..." + }, + "calculatingRisks": { + "message": "Calculando os riscos..." + }, + "generatingReports": { + "message": "Gerando os relatórios..." + }, + "compilingInsightsProgress": { + "message": "Compilando conhecimentos..." + }, + "reportGenerationDone": { + "message": "Pronto!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5431,7 +5446,7 @@ "message": "Número mínimo de palavras" }, "passwordTypePolicyOverride": { - "message": "Password type", + "message": "Tipo da senha", "description": "Name of the password generator policy that overrides the user's password/passphrase selection." }, "userPreference": { @@ -5714,7 +5729,7 @@ } }, "sendCreatedDescriptionPassword": { - "message": "Copy and share this Send link. The Send will be available to anyone with the link and password you set for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. O Send ficará disponível para qualquer um com o link e a senha por $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5724,7 +5739,7 @@ } }, "sendCreatedDescriptionEmail": { - "message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.", + "message": "Copie e compartilhe este link do Send. Ele pode ser visto pelas pessoas que você especificou pelos próximos $TIME$.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "time": { @@ -5840,10 +5855,6 @@ "message": "Não sabe a senha? Peça ao remetente a senha necessária para acessar esse Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send é oculto por padrão. Você pode alternar a visibilidade usando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Baixar anexos" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Eu aceito estes riscos e atualizações de política" }, + "autoConfirmEnabledByAdmin": { + "message": "Ativou a configuração de confirmação de usuários automática" + }, + "autoConfirmDisabledByAdmin": { + "message": "Desativou a configuração de confirmação de usuários automática" + }, + "autoConfirmEnabledByPortal": { + "message": "Adicionou a política de confirmação de usuários automática" + }, + "autoConfirmDisabledByPortal": { + "message": "Removeu a política de confirmação de usuários automática" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre individual" }, @@ -6429,6 +6455,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Confirme seu e-mail para ver este Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "O usuário Bitwarden que criou este Send optou por ocultar seu endereço de e-mail. Você deve certificar-se de que confia na fonte deste link antes de usar ou baixar seu conteúdo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6674,10 +6704,10 @@ } }, "reinviteSuccessToast": { - "message": "1 invitation sent" + "message": "1 convite enviado" }, "bulkReinviteSentToast": { - "message": "$COUNT$ invitations sent", + "message": "$COUNT$ convites enviados", "placeholders": { "count": { "content": "$1", @@ -6703,7 +6733,7 @@ } }, "bulkReinviteProgressTitle": { - "message": "$COUNT$ of $TOTAL$ invitations sent...", + "message": "$COUNT$ dos $TOTAL$ convites foram enviados...", "placeholders": { "count": { "content": "$1", @@ -6716,10 +6746,10 @@ } }, "bulkReinviteProgressSubtitle": { - "message": "Keep this page open until all are sent." + "message": "Mantenha esta página aberta até que todos sejam enviados." }, "bulkReinviteFailuresTitle": { - "message": "$COUNT$ invitations didn't send", + "message": "$COUNT$ convites não foram enviados", "placeholders": { "count": { "content": "$1", @@ -6728,10 +6758,10 @@ } }, "bulkReinviteFailureTitle": { - "message": "1 invitation didn't send" + "message": "1 convite não foi enviado" }, "bulkReinviteFailureDescription": { - "message": "An error occurred while sending invitations to $COUNT$ of $TOTAL$ members. Try sending again, and if the problem continues,", + "message": "Ocorreu um erro ao enviar $COUNT$ convites para os $TOTAL$ membros. Tente enviar de novo, e se o problema continuar,", "placeholders": { "count": { "content": "$1", @@ -6744,7 +6774,7 @@ } }, "bulkResendInvitations": { - "message": "Try sending again" + "message": "Tentar enviar de novo" }, "bulkRemovedMessage": { "message": "Removido com sucesso" @@ -7079,16 +7109,16 @@ "message": "Uma ou mais políticas da organização impedem que você exporte seu cofre individual." }, "activateAutofillPolicy": { - "message": "Activate autofill" + "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDescription": { - "message": "Activate the autofill on page load setting on the browser extension for all existing and new members." + "message": "Ative a configuração de preenchimento automático no carregamento da página na extensão do navegador para todos os membros existentes e novos." }, "autofillOnPageLoadExploitWarning": { - "message": "Compromised or untrusted websites can exploit autofill on page load." + "message": "Sites comprometidos ou não confiáveis podem explorar do preenchimento automático ao carregar a página." }, "learnMoreAboutAutofillPolicy": { - "message": "Learn more about autofill" + "message": "Saiba mais sobre preenchimento automático" }, "selectType": { "message": "Selecionar tipo de SSO" @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "Email ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -10185,7 +10218,7 @@ "message": "Atribuir tarefas" }, "allTasksAssigned": { - "message": "All tasks have been assigned" + "message": "Todas as tarefas foram atribuídas" }, "assignSecurityTasksToMembers": { "message": "Envie notificações para alteração de senhas" @@ -10595,7 +10628,7 @@ "message": "Falha ao salvar a integração. Tente novamente mais tarde." }, "mustBeOrganizationOwnerAdmin": { - "message": "You must be an Organization Owner or Admin to perform this action." + "message": "Você precisa ser proprietário ou administrador da organização para executar esta ação." }, "mustBeOrgOwnerToPerformAction": { "message": "Você precisa ser o proprietário da organização para executar esta ação." @@ -11526,13 +11559,13 @@ "message": "O Bitwarden tentará reivindicar o domínio 3 vezes durante as primeiras 72 horas. Se o domínio não poder ser reivindicado, confira o registro de DNS no seu servidor e reivindique manualmente. Se não for reivindicado, o domínio será removido da sua organização em 7 dias." }, "automaticDomainClaimProcess1": { - "message": "Bitwarden will attempt to claim the domain within 72 hours. If the domain can't be claimed, verify your DNS record and claim manually. Unclaimed domains are removed after 7 days." + "message": "Bitwarden tentará reivindicar o domínio dentro de 72 horas. Se o domínio não puder ser reivindicado, verifique o seu registro DNS e reivindique manualmente. Domínios não reivindicados são removidos após 7 dias." }, "automaticDomainClaimProcess2": { - "message": "Once claimed, existing members with claimed domains will be emailed about the " + "message": "Ao reivindicar, os membros existentes com domínios reivindicados serão enviados um e-mail sobre a " }, "accountOwnershipChange": { - "message": "account ownership change" + "message": "alteração de propriedade da conta" }, "automaticDomainClaimProcessEnd": { "message": "." @@ -11550,7 +11583,7 @@ "message": "Reivindicado" }, "domainStatusPending": { - "message": "Pending" + "message": "Pendente" }, "claimedDomainsDescription": { "message": "Reivindique um domínio para ser o proprietário das contas dos membros. A página do identificador do SSO será pulada durante a autenticação dos membros com os domínios reivindicados, e os administradores poderão apagar contas reivindicadas." @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais de busca e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi enviado para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "bulkArchiveItems": { "message": "Itens arquivados" @@ -12583,7 +12613,7 @@ "message": "Tem certeza que deseja continuar?" }, "errorCannotDecrypt": { - "message": "Error: Cannot decrypt" + "message": "Erro: Não é possível descriptografar" }, "userVerificationFailed": { "message": "Falha na verificação do usuário." @@ -12847,17 +12877,35 @@ "storageFullDescription": { "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { - "message": "Who can view" + "message": "Quem pode visualizar" }, "specificPeople": { - "message": "Specific people" + "message": "Pessoas específicas" }, "emailVerificationDesc": { - "message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send." + "message": "Após compartilhar este link de Send, indivíduos precisarão verificar seus e-mails com um código para visualizar este Send." }, "enterMultipleEmailsSeparatedByComma": { - "message": "Enter multiple emails by separating with a comma." + "message": "Digite vários e-mails, separados com uma vírgula." + }, + "emailsRequiredChangeAccessType": { + "message": "A verificação de e-mail requer pelo menos um endereço de e-mail. Para remover todos, altere o tipo de acesso acima." }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" @@ -12866,7 +12914,7 @@ "message": "Quando você remover o armazenamento, você receberá um crédito de conta proporcional que irá automaticamente para sua próxima fatura." }, "ownerBadgeA11yDescription": { - "message": "Owner, $OWNER$, show all items owned by $OWNER$", + "message": "Proprietário, $OWNER$, mostrar todos os itens pertencentes a $OWNER$", "placeholders": { "owner": { "content": "$1", @@ -12878,35 +12926,47 @@ "message": "Você tem o Premium" }, "emailProtected": { - "message": "E-mail protegido" + "message": "Protegido por e-mail" }, "invalidSendPassword": { - "message": "Invalid Send password" + "message": "Senha do Send inválida" + }, + "vaultWelcomeDialogTitle": { + "message": "Você entrou! Boas-vindas ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas senhas e informações pessoais no seu cofre do Bitwarden. Vamos te dar um guia." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Começar guia" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Pular" }, "sendPasswordHelperText": { - "message": "Individuals will need to enter the password to view this Send", + "message": "Os indivíduos precisarão digitar a senha para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "perUser": { - "message": "per user" + "message": "por usuário" }, "upgradeToTeams": { - "message": "Upgrade to Teams" + "message": "Fazer upgrade para o Equipes" }, "upgradeToEnterprise": { - "message": "Upgrade to Enterprise" + "message": "Fazer upgrade para o Empresarial" }, "upgradeShareEvenMore": { - "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise" + "message": "Compartilhe ainda mais com o Famílias, ou receba segurança poderosa e confiável de senhas com o Equipes ou o Empresarial" }, "organizationUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Os preços excluem os impostos e são cobrados anualmente." }, "invoicePreviewErrorMessage": { - "message": "Encountered an error while generating the invoice preview." + "message": "Foi deparado um erro ao gerar a pré-visualização da fatura." }, "planProratedMembershipInMonths": { - "message": "Prorated $PLAN$ membership ($NUMOFMONTHS$)", + "message": "Assinatura $PLAN$ rateada ($NUMOFMONTHS$)", "placeholders": { "plan": { "content": "$1", @@ -12919,16 +12979,16 @@ } }, "premiumSubscriptionCredit": { - "message": "Premium subscription credit" + "message": "Crédito da assinatura Premium" }, "enterpriseMembership": { - "message": "Enterprise membership" + "message": "Assinatura Empresarial" }, "teamsMembership": { - "message": "Teams membership" + "message": "Assinatura do Equipes" }, "plansUpdated": { - "message": "You've upgraded to $PLAN$!", + "message": "Você fez upgrade para o $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -12937,6 +12997,24 @@ } }, "paymentMethodUpdateError": { - "message": "There was an error updating your payment method." + "message": "Houve um erro ao atualizar seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Senha inválida. Peça ao remetente a senha necessária para acessar este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este send expirá às $TIME$ em $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index e0626be7255..55b35c60155 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "A subscrição foi restabelecida." }, + "resubscribe": { + "message": "Renovar a subscrição" + }, + "yourSubscriptionIsExpired": { + "message": "A sua subscrição expirou" + }, + "yourSubscriptionIsCanceled": { + "message": "A sua subscrição foi cancelada" + }, "cancelConfirmation": { "message": "Tem a certeza de que pretende cancelar? Perderá o acesso a todas as funcionalidades desta subscrição no final deste ciclo de faturação." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Utilizador $ID$ confirmado automaticamente.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizador $ID$ editado.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "A gerar a sua Inteligência de Acesso..." }, - "fetchingMemberData": { - "message": "A obter dados dos membros..." - }, - "analyzingPasswordHealth": { - "message": "A analisar a segurança da palavra-passe..." - }, - "calculatingRiskScores": { - "message": "A calcular pontuações de risco..." - }, - "generatingReportData": { - "message": "A gerar dados do relatório..." - }, - "savingReport": { - "message": "A guardar relatório..." - }, - "compilingInsights": { - "message": "A compilar insights..." - }, "loadingProgress": { "message": "A carregar progresso" }, - "thisMightTakeFewMinutes": { - "message": "Isto pode demorar alguns minutos." + "reviewingMemberData": { + "message": "A rever os dados dos membros..." + }, + "analyzingPasswords": { + "message": "A analisar palavras-passe..." + }, + "calculatingRisks": { + "message": "A calcular riscos..." + }, + "generatingReports": { + "message": "A gerar relatórios..." + }, + "compilingInsightsProgress": { + "message": "A compilar insights..." + }, + "reportGenerationDone": { + "message": "Concluído!" }, "riskInsightsRunReport": { "message": "Executar relatório" @@ -5840,10 +5855,6 @@ "message": "Não sabe a palavra-passe? Peça ao remetente a palavra-passe necessária para aceder a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Este Send está oculto por defeito. Pode alternar a sua visibilidade utilizando o botão abaixo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Transferir anexos" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Aceito estes riscos e atualizações da política" }, + "autoConfirmEnabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores ativada" + }, + "autoConfirmDisabledByAdmin": { + "message": "Definição de confirmação automática de utilizadores desativada" + }, + "autoConfirmEnabledByPortal": { + "message": "Política de confirmação automática de utilizadores adicionada" + }, + "autoConfirmDisabledByPortal": { + "message": "Política de confirmação automática de utilizadores removida" + }, + "system": { + "message": "Sistema" + }, "personalOwnership": { "message": "Remover cofre pessoal" }, @@ -6429,6 +6455,10 @@ "message": "Ver Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Confirme o seu e-mail para ver este Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "O utilizador Bitwarden que criou este Send optou por ocultar o seu endereço de e-mail. Deve certificar-se de que confia na fonte deste link antes de utilizar ou descarregar o seu conteúdo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Código de verificação inválido" }, + "invalidEmailOrVerificationCode": { + "message": "E-mail ou código de verificação inválido" + }, "keyConnectorDomain": { "message": "Domínio do Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Os itens arquivados aparecerão aqui e serão excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático." }, - "itemWasSentToArchive": { - "message": "O item foi movido para o arquivo" + "itemArchiveToast": { + "message": "Item arquivado" }, - "itemWasUnarchived": { - "message": "O item foi desarquivado" - }, - "itemUnarchived": { - "message": "O item foi desarquivado" + "itemUnarchivedToast": { + "message": "Item desarquivado" }, "bulkArchiveItems": { "message": "Itens arquivados" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Utilizou os $GB$ GB do seu armazenamento encriptado. Para continuar a guardar ficheiros, adicione mais espaço de armazenamento." }, + "extensionPromptHeading": { + "message": "Obtenha a extensão para aceder facilmente ao seu cofre" + }, + "extensionPromptBody": { + "message": "Com a extensão do navegador instalada, terá o Bitwarden sempre disponível online. Esta preencherá automaticamente as palavras-passe, para que possa iniciar sessão nas suas contas com um único clique." + }, + "extensionPromptImageAlt": { + "message": "Um navegador web a apresentar a extensão Bitwarden com itens de preenchimento automático para a página atual." + }, + "skip": { + "message": "Saltar" + }, + "downloadExtension": { + "message": "Transferir extensão" + }, "whoCanView": { "message": "Quem pode ver" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Introduza vários e-mails, separados por vírgula." }, + "emailsRequiredChangeAccessType": { + "message": "A verificação por e-mail requer pelo menos um endereço de e-mail. Para remover todos os e-mails, altere o tipo de acesso acima." + }, "emailPlaceholder": { "message": "utilizador@bitwarden.com , utilizador@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Palavra-passe do Send inválida" }, + "vaultWelcomeDialogTitle": { + "message": "Entrou com sucesso! Bem-vindo ao Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Armazene todas as suas palavras-passe e informações pessoais no seu cofre Bitwarden. Vamos mostrar-lhe como funciona." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Iniciar tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Saltar" + }, "sendPasswordHelperText": { "message": "Os indivíduos terão de introduzir a palavra-passe para ver este Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Ocorreu um erro ao atualizar o seu método de pagamento." + }, + "sendPasswordInvalidAskOwner": { + "message": "Palavra-passe inválida. Peça ao remetente a palavra-passe necessária para aceder a este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Este Send expira às $TIME$ de $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 0b567f2f969..8fe372fbc52 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonamentul a fost restabilit." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Sigur doriți să anulați? Veți pierde accesul la toate funcționalitățile acestui abonament la sfârșitul acestui ciclu de facturare." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Utilizatorul $ID$ a fost editat.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Nu știți parola? Solicitați expeditorului parola necesară pentru a accesa acest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Acest Send este ascuns în mod implicit. Puteți comuta vizibilitatea acestuia cu butonul de mai jos.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Înlăturați seiful personal" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Utilizatorul Bitwarden care a creat acest Send a ales să-și ascundă adresa de e-mail. Ar trebui să vă asigurați că aveți încredere în sursa acestui link înainte de utilizarea sau descărcarea conținutului acestuia.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 5433c0ac312..6687b59fc86 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Подписка восстановлена." }, + "resubscribe": { + "message": "Возобновить подписку" + }, + "yourSubscriptionIsExpired": { + "message": "Срок действия вашей подписки истек" + }, + "yourSubscriptionIsCanceled": { + "message": "Ваша подписка отменена" + }, "cancelConfirmation": { "message": "Вы действительно хотите отменить? Вы потеряете доступ ко всем возможностям этой подписки в конце этого платежного периода." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Автоматически подтвержденный пользователь $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Изменен пользователь $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Ваша информация о доступе генерируется..." }, - "fetchingMemberData": { - "message": "Получение данных о пользователях..." - }, - "analyzingPasswordHealth": { - "message": "Анализ здоровья пароля..." - }, - "calculatingRiskScores": { - "message": "Расчет показателей риска..." - }, - "generatingReportData": { - "message": "Генерация данных отчета..." - }, - "savingReport": { - "message": "Сохранение отчета..." - }, - "compilingInsights": { - "message": "Компиляция информации..." - }, "loadingProgress": { "message": "Прогресс загрузки" }, - "thisMightTakeFewMinutes": { - "message": "Это может занять несколько минут." + "reviewingMemberData": { + "message": "Проверка данных пользователя..." + }, + "analyzingPasswords": { + "message": "Анализ паролей..." + }, + "calculatingRisks": { + "message": "Расчет рисков..." + }, + "generatingReports": { + "message": "Формирование отчетов..." + }, + "compilingInsightsProgress": { + "message": "Компиляция информации..." + }, + "reportGenerationDone": { + "message": "Готово!" }, "riskInsightsRunReport": { "message": "Запустить отчет" @@ -5840,10 +5855,6 @@ "message": "Не знаете пароль? Для доступа к этой Send, запросите его у отправителя.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Эта Send по умолчанию скрыта. Вы можете переключить ее видимость с помощью кнопки ниже.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Скачать вложения" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Я принимаю эти риски и политики обновления" }, + "autoConfirmEnabledByAdmin": { + "message": "Включена настройка автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByAdmin": { + "message": "Отключена настройка автоматического подтверждения пользователей" + }, + "autoConfirmEnabledByPortal": { + "message": "Добавлена политика автоматического подтверждения пользователей" + }, + "autoConfirmDisabledByPortal": { + "message": "Удалена политика автоматического подтверждения пользователей" + }, + "system": { + "message": "Система" + }, "personalOwnership": { "message": "Удалить личное хранилище" }, @@ -6429,6 +6455,10 @@ "message": "Просмотр Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Подтвердите свой адрес email, чтобы просмотреть эту Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Пользователь Bitwarden, создавший эту Send, решил скрыть свой адрес email. Вы должны убедиться, что доверяете источнику этой ссылки, прежде чем использовать или скачивать ее содержимое.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неверный код подтверждения" }, + "invalidEmailOrVerificationCode": { + "message": "Неверный email или код подтверждения" + }, "keyConnectorDomain": { "message": "Домен соединителя ключей" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Архивированные элементы появятся здесь и будут исключены из общих результатов поиска и предложений автозаполнения." }, - "itemWasSentToArchive": { - "message": "Элемент был отправлен в архив" + "itemArchiveToast": { + "message": "Элемент архивирован" }, - "itemWasUnarchived": { - "message": "Элемент был разархивирован" - }, - "itemUnarchived": { - "message": "Элемент был разархивирован" + "itemUnarchivedToast": { + "message": "Элемент разархивирован" }, "bulkArchiveItems": { "message": "Элементы архивированы" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Вы использовали все $GB$ вашего зашифрованного хранилища. Чтобы продолжить хранение файлов, добавьте дополнительное хранилище." }, + "extensionPromptHeading": { + "message": "Установите расширение для удобного доступа к хранилищу" + }, + "extensionPromptBody": { + "message": "Установив расширение для браузера, вы сможете использовать Bitwarden везде, где есть интернет. Оно будет вводить пароли, так что вы сможете входить в свои аккаунты одним щелчком мыши." + }, + "extensionPromptImageAlt": { + "message": "Браузер, отображающий расширение Bitwarden с элементами автозаполнения для текущей веб-страницы." + }, + "skip": { + "message": "Пропустить" + }, + "downloadExtension": { + "message": "Скачать расширение" + }, "whoCanView": { "message": "Кто может просматривать" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введите несколько email, разделяя их запятой." }, + "emailsRequiredChangeAccessType": { + "message": "Для проверки электронной почты требуется как минимум один адрес email. Чтобы удалить все адреса электронной почты, измените тип доступа выше." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Неверный пароль Send" }, + "vaultWelcomeDialogTitle": { + "message": "Вы с нами! Добро пожаловать в Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Сохраняйте все свои пароли и личную информацию в хранилище Bitwarden. Мы покажем как это работает." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Начать знакомство" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Пропустить" + }, "sendPasswordHelperText": { "message": "Пользователям необходимо будет ввести пароль для просмотра этой Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Произошла ошибка при обновлении способа оплаты." + }, + "sendPasswordInvalidAskOwner": { + "message": "Неверный пароль. Для доступа к этой Send, запросите его у отправителя.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index ac073315340..3bc5fa153d9 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index dffa3975792..6a25d6d0f3f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Predplatné bolo obnovené." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Naozaj chcete zrušiť? Stratíte prístup k všetkým funkciám, ktoré vám predplatné ponúka na konci fakturačného obdobia." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Používateľ $ID$ upravený.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generuje sa prehľad o prístupe..." }, - "fetchingMemberData": { - "message": "Sťahujú sa dáta o členoch..." - }, - "analyzingPasswordHealth": { - "message": "Analyzuje sa odolnosť hesiel..." - }, - "calculatingRiskScores": { - "message": "Vypočítava sa úroveň ohrozenia..." - }, - "generatingReportData": { - "message": "Generujú sa dáta reportu..." - }, - "savingReport": { - "message": "Ukladá sa report..." - }, - "compilingInsights": { - "message": "Kompiluje sa prehľad..." - }, "loadingProgress": { "message": "Priebeh načítania" }, - "thisMightTakeFewMinutes": { - "message": "Môže to trvať niekoľko minút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Generovať report" @@ -5840,10 +5855,6 @@ "message": "Neviete heslo? Požiadajte odosielateľa o heslo potrebné k prístupu k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Tento Send je normálne skrytý. Tlačidlom nižšie môžete prepnúť jeho viditeľnosť.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Stiahnuť prílohy" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Akceptujem tieto riziká a aktualizácie pravidiel" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Zakázať osobný trezor" }, @@ -6429,6 +6455,10 @@ "message": "Zobraziť Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Používateľ Bitwardenu, ktorý vytvoril tento Send, skryl e-mailové adresy pred príjemcami. Mali by ste zvážiť, či dôverujete zdrojovému odkazu pred jeho použitím alebo stiahnutím jeho obsahu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Doména Key Connectora" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, - "itemWasSentToArchive": { - "message": "Položka bola archivovaná" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Položka bola odobraná z archívu" - }, - "itemUnarchived": { - "message": "Položka bola odobraná z archívu" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Položky archivované" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Použili ste všetkých $GB$ GB vášho šifrovaného úložiska. Ak chcete uložiť ďalšie súbory, pridajte viac úložiska." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index aef3a8ca3ac..4553f68c54b 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Naročnina je bila ponovno vzpostavljena." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ste prepričani, da želite odpovedati? Ob koncu plačilnega obdobja boste izgubili dostop do vseh ugodnosti naročnine." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 7e7bcbd97b1..24ff0eb2f9f 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index d457b16f44e..f0878f973c2 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Претплата је враћена." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Јесте ли сигурни да хоћете да откажете? На крају овог обрачунског циклуса изгубићете приступ свим функцијама ове претплате." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Корисник $ID$ промењен.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Преузимање података о члановима..." - }, - "analyzingPasswordHealth": { - "message": "Анализа здравља лозинки..." - }, - "calculatingRiskScores": { - "message": "Израчунавање резултата ризика..." - }, - "generatingReportData": { - "message": "Генерисање података извештаја..." - }, - "savingReport": { - "message": "Чување извештаја..." - }, - "compilingInsights": { - "message": "Састављање увида..." - }, "loadingProgress": { "message": "Учитавање напретка" }, - "thisMightTakeFewMinutes": { - "message": "Ово може потрајати неколико минута." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Покрените извештај" @@ -5840,10 +5855,6 @@ "message": "Не знате лозинку? Затражите од пошиљаоца лозинку потребну за приступ овом Слању.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Ово Слање је подразумевано скривено. Можете да пребацујете његову видљивост помоћу дугмета испод.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Преузмите прилоге" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Прихватам ове ризике и ажурирања политика" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Лично власништво" }, @@ -6429,6 +6455,10 @@ "message": "Видети Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden корисник који је створио овај „Send“ је изабрао да сакрије своју е-адресу. Требате да се осигурате да верујете извору ове везе пре употребе или преузимања његовог садржаја.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Неисправан верификациони код" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен конектора кључа" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Архивиране ставке ће се овде појавити и бити искључени из општих резултата претраге и сугестија о ауто-пуњењу." }, - "itemWasSentToArchive": { - "message": "Ставка је послата у архиву" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Ставке у архиви" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index f4bab640104..77771bb166d 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonnemanget har återupptagits." }, + "resubscribe": { + "message": "Prenumerera igen" + }, + "yourSubscriptionIsExpired": { + "message": "Din prenumeration har löpt ut" + }, + "yourSubscriptionIsCanceled": { + "message": "Din prenumeration är avbruten" + }, "cancelConfirmation": { "message": "Är du säker på att du vill avsluta? Du kommer förlora tillgång till alla funktioner som abonnemanget erbjuder vid slutet av den nuvarande faktureringsperioden." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Redigerade användaren $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Genererar din Access Intelligence..." }, - "fetchingMemberData": { - "message": "Hämtar medlemsdata..." - }, - "analyzingPasswordHealth": { - "message": "Analyserar lösenordshälsa..." - }, - "calculatingRiskScores": { - "message": "Beräknar riskpoäng..." - }, - "generatingReportData": { - "message": "Genererar rapportdata..." - }, - "savingReport": { - "message": "Sparar rapport..." - }, - "compilingInsights": { - "message": "Sammanställer insikter..." - }, "loadingProgress": { "message": "Inläsningsförlopp" }, - "thisMightTakeFewMinutes": { - "message": "Detta kan ta några minuter." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Kör rapport" @@ -5840,10 +5855,6 @@ "message": "Vet du inte lösenordet? Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Denna Send är dold som standard. Du kan växla dess synlighet med hjälp av knappen nedan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ladda ner bilagor" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Jag accepterar dessa risker och policyuppdateringar" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Radera individuellt valv" }, @@ -6429,6 +6455,10 @@ "message": "Visa Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bitwarden-användaren som skapade denna Send har valt att dölja sin e-postadress. Du bör se till att du litar på källan till denna länk innan du använder eller hämtar innehållet.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector-domän" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Arkiverade objekt visas här och undantas från allmänna sökresultat och autofyllförslag." }, - "itemWasSentToArchive": { - "message": "Objektet skickades till arkivet" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Objektet har avarkiverats" - }, - "itemUnarchived": { - "message": "Objektet har avarkiverats" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Objekt arkiverade" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "Du har använt alla $GB$ GB av din krypterade lagring. För att fortsätta lagra filer, lägg till mer lagringsutrymme." }, + "extensionPromptHeading": { + "message": "Skaffa tillägget för enkel åtkomst till valv" + }, + "extensionPromptBody": { + "message": "Med webbläsartillägget installerat tar du Bitwarden överallt på nätet. Det fyller i lösenord, så att du kan logga in på dina konton med ett enda klick." + }, + "extensionPromptImageAlt": { + "message": "En webbläsare som visar Bitwarden-tillägget med autofyll objekt för den aktuella webbsidan." + }, + "skip": { + "message": "Hoppa över" + }, + "downloadExtension": { + "message": "Ladda ner tillägg" + }, "whoCanView": { "message": "Vem kan se" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Ange flera e-postadresser genom att separera dem med kommatecken." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "användare@bitwarden.com , användare@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Ogiltigt Send-lösenord" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individer måste ange lösenordet för att visa denna Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Ogiltigt lösenord. Fråga avsändaren om lösenordet som behövs för att komma åt denna Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index b93591182db..757a8158097 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "சந்தா மீட்டெடுக்கப்பட்டது." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "நீங்கள் ரத்துசெய்ய விரும்புகிறீர்களா? இந்த பில்லிங் சுழற்சியின் முடிவில் இந்தச் சந்தாவின் அனைத்து அம்சங்களுக்கான அணுகலையும் நீங்கள் இழப்பீர்கள்." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "பயனர் $ID$ திருத்தப்பட்டார்.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "கடவுச்சொல் தெரியவில்லையா? இந்த Send-ஐ அணுகத் தேவையான கடவுச்சொல்லை அனுப்புநரிடம் கேட்கவும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "இந்த Send இயல்பாக மறைக்கப்பட்டுள்ளது. கீழே உள்ள பொத்தானைப் பயன்படுத்தி அதன் தெரிவுநிலையை மாற்றலாம்.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "இணைப்புகளைப் பதிவிறக்கவும்" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "தனிப்பட்ட வால்ட்டை அகற்று" }, @@ -6429,6 +6455,10 @@ "message": "Send ஐப் பார்க்கவும்", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "இந்த Send-ஐ உருவாக்கிய Bitwarden பயனர் தங்கள் மின்னஞ்சல் முகவரியை மறைக்கத் தேர்ந்தெடுத்துள்ளார். இதன் உள்ளடக்கத்தைப் பயன்படுத்துவதற்கு அல்லது பதிவிறக்குவதற்கு முன், இந்த இணைப்பின் மூலத்தை நீங்கள் நம்புகிறீர்கள் என்பதை உறுதிப்படுத்த வேண்டும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "தவறான சரிபார்ப்புக் குறியீடு" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "கீ கனெக்டர் டொமைன்" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index b95256dfacd..4e6a8265ad6 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index e5e449038ac..2eb0a00f8fc 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "The subscription has been reinstated." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Edited user $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Don't know the password? Ask the sender for the password needed to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "This Send is hidden by default. You can toggle its visibility using the button below.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Download attachments" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -6429,6 +6455,10 @@ "message": "View Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Invalid verification code" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector domain" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 97a29756a89..260514462cb 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Abonelik sürdürüldü." }, + "resubscribe": { + "message": "Yeniden abone ol" + }, + "yourSubscriptionIsExpired": { + "message": "Aboneliğiniz sona erdi" + }, + "yourSubscriptionIsCanceled": { + "message": "Aboneliğiniz iptal edildi" + }, "cancelConfirmation": { "message": "İptal etmek istediğinden emin misin? Bu fatura döneminin sonunda bu aboneliğin tüm özelliklerine erişiminizi kaybedeceksiniz." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Kullanıcı düzenlendi: $ID$.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Access Intelligence’ınız oluşturuluyor..." }, - "fetchingMemberData": { - "message": "Üye verileri getiriliyor..." - }, - "analyzingPasswordHealth": { - "message": "Parola sağlığı analiz ediliyor..." - }, - "calculatingRiskScores": { - "message": "Risk puanları hesaplanıyor..." - }, - "generatingReportData": { - "message": "Rapor verileri oluşturuluyor..." - }, - "savingReport": { - "message": "Rapor kaydediliyor..." - }, - "compilingInsights": { - "message": "İçgörüler derleniyor..." - }, "loadingProgress": { "message": "Yükleme ilerlemesi" }, - "thisMightTakeFewMinutes": { - "message": "Bu işlem birkaç dakika sürebilir." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Raporu çalıştır" @@ -5840,10 +5855,6 @@ "message": "Parolayı bilmiyor musunuz? Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Bu Send varsayılan olarak gizlidir. Aşağıdaki düğmeyi kullanarak görünürlüğünü değiştirebilirsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Ekleri indir" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Bu riskleri ve ilke güncellemelerini kabul ediyorum" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "Sistem" + }, "personalOwnership": { "message": "Kişisel kasayı kaldır" }, @@ -6429,6 +6455,10 @@ "message": "Send'i görüntüle", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Bu Send'i görmek için e-posta adresinizi doğrulayın", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Bu Send'i oluşturan Bitwarden kullanıcısı e-posta adresini gizlemeyi seçti. Kullanmadan veya içeriğini indirmeden önce bu bağlantının kaynağının güvenilir olduğundan emin olun.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, + "invalidEmailOrVerificationCode": { + "message": "E-posta veya doğrulama kodu geçersiz" + }, "keyConnectorDomain": { "message": "Key Connector alan adı" }, @@ -11896,13 +11929,10 @@ "noItemsInArchiveDesc": { "message": "Arşivlenmiş kayıtlar burada görünür ve genel arama sonuçları ile otomatik doldurma önerilerinden hariç tutulur." }, - "itemWasSentToArchive": { - "message": "Kayıt arşive gönderildi" + "itemArchiveToast": { + "message": "Kayıt arşivlendi" }, - "itemWasUnarchived": { - "message": "Kayıt arşivden çıkarıldı" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "Kayıt arşivden çıkarıldı" }, "bulkArchiveItems": { @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Kim görebilir" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "E-posta adreslerini virgülle ayırarak yazın." }, + "emailsRequiredChangeAccessType": { + "message": "E-posta doğrulaması için en az bir e-posta adresi gerekir. Tüm e-postaları silmek için yukarıdan erişim türünü değiştirin." + }, "emailPlaceholder": { "message": "kullanici@bitwarden.com , kullanici@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Geçersiz Send parolası" }, + "vaultWelcomeDialogTitle": { + "message": "Bitwarden'a hoş geldiniz" + }, + "vaultWelcomeDialogDescription": { + "message": "Tüm parolalarınızı ve kişisel bilgilerinizi Bitwarden kasanızda saklayabilirsiniz. Size etrafı gezdirelim." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Tura başla" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Geç" + }, "sendPasswordHelperText": { "message": "Bu Send'i görmek isteyen kişilerin parola girmesi gerekecektir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "Ödeme yönteminizi güncellerken bir hata oluştu." + }, + "sendPasswordInvalidAskOwner": { + "message": "Parola geçersiz. Bu Send'e erişmek için gereken parolayı dosyayı gönderen kişiye sorabilirsiniz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "Bu Send'in süresi $DATE$ $TIME$ tarihinde dolacaktır", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 6fc3db5d64e..7dc407ad5e6 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Передплату було відновлено." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Ви справді хочете скасувати? Ви втратите доступ до всіх можливостей, пов'язаних з нею після завершення поточного періоду передплати." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Користувача $ID$ змінено.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, - "fetchingMemberData": { - "message": "Fetching member data..." - }, - "analyzingPasswordHealth": { - "message": "Analyzing password health..." - }, - "calculatingRiskScores": { - "message": "Calculating risk scores..." - }, - "generatingReportData": { - "message": "Generating report data..." - }, - "savingReport": { - "message": "Saving report..." - }, - "compilingInsights": { - "message": "Compiling insights..." - }, "loadingProgress": { "message": "Loading progress" }, - "thisMightTakeFewMinutes": { - "message": "This might take a few minutes." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Run report" @@ -5840,10 +5855,6 @@ "message": "Не знаєте пароль? Попросіть його у відправника для отримання доступу.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Це відправлення типово приховане. Ви можете змінити його видимість кнопкою нижче.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Завантажити вкладення" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "I accept these risks and policy updates" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Вилучити особисте сховище" }, @@ -6429,6 +6455,10 @@ "message": "Переглянути відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Користувач Bitwarden, який створив це відправлення, вирішив приховати свою адресу електронної пошти. Вам слід упевнитися в надійності джерела цього посилання перед його використанням чи завантаженням вмісту.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Домен Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Архівовані записи з'являтимуться тут і будуть виключені з результатів звичайного пошуку та пропозицій автозаповнення." }, - "itemWasSentToArchive": { - "message": "Item was sent to archive" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Item was unarchived" - }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Items archived" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Хто може переглядати" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Введіть декілька адрес е-пошти, розділяючи їх комою." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index d54ae1933a7..3671c9e8ab5 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "Đã kích hoạt lại gói." }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "Bạn có chắc muốn hủy không? Bạn sẽ mất hết quyền truy cập tất cả các tính năng của thuê bao này khi kì thanh toán kết thúc." }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "Người dùng $ID$ đã được chỉnh sửa.", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "Đang tạo Access Intelligence của bạn..." }, - "fetchingMemberData": { - "message": "Đang lấy dữ liệu thành viên..." - }, - "analyzingPasswordHealth": { - "message": "Đang phân tích độ mạnh mật khẩu..." - }, - "calculatingRiskScores": { - "message": "Đang tính điểm rủi ro..." - }, - "generatingReportData": { - "message": "Đang tạo dữ liệu báo cáo..." - }, - "savingReport": { - "message": "Đang lưu báo cáo..." - }, - "compilingInsights": { - "message": "Đang biên soạn thông tin chi tiết..." - }, "loadingProgress": { "message": "Đang tải tiến trình" }, - "thisMightTakeFewMinutes": { - "message": "Quá trình này có thể mất vài phút." + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "Chạy báo cáo" @@ -5840,10 +5855,6 @@ "message": "Không biết mật khẩu? Hãy yêu cầu người gửi cung cấp mật khẩu cần thiết để truy cập vào Send này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "Send này sẽ bị ẩn theo mặc định. Bạn có thể bật/tắt tính năng này bằng cách nhấn vào nút bên dưới.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "Tải xuống tập tin đính kèm" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "Tôi chấp nhận những rủi ro và cập nhật chính sách này" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "Xóa kho lưu trữ riêng lẻ" }, @@ -6429,6 +6455,10 @@ "message": "Xem Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "Người dùng Bitwarden đã tạo Send này đã chọn ẩn địa chỉ email của họ. Bạn nên đảm bảo rằng bạn tin tưởng nguồn của liên kết này trước khi sử dụng hoặc tải xuống nội dung của nó.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Tên miền Key Connector" }, @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "Các mục đã lưu trữ sẽ hiển thị ở đây và sẽ bị loại khỏi kết quả tìm kiếm và gợi ý tự động điền." }, - "itemWasSentToArchive": { - "message": "Mục đã được chuyển vào kho lưu trữ" + "itemArchiveToast": { + "message": "Item archived" }, - "itemWasUnarchived": { - "message": "Mục đã được bỏ lưu trữ" - }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "Các mục đã được lưu trữ" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "Who can view" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "Enter multiple emails by separating with a comma." }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Invalid Send password" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "Individuals will need to enter the password to view this Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 76b95446091..ff23e62c5d8 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -3338,6 +3338,15 @@ "reinstated": { "message": "您的订阅已恢复。" }, + "resubscribe": { + "message": "重新订阅" + }, + "yourSubscriptionIsExpired": { + "message": "您的订阅已过期" + }, + "yourSubscriptionIsCanceled": { + "message": "您的订阅已取消" + }, "cancelConfirmation": { "message": "确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "自动确认了用户 $ID$。", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "编辑了用户 $ID$。", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在生成 Access Intelligence..." }, - "fetchingMemberData": { - "message": "正在获取成员数据..." - }, - "analyzingPasswordHealth": { - "message": "正在分析密码健康度..." - }, - "calculatingRiskScores": { - "message": "正在计算风险评分..." - }, - "generatingReportData": { - "message": "正在生成报告数据..." - }, - "savingReport": { - "message": "正在保存报告..." - }, - "compilingInsights": { - "message": "正在编译洞察..." - }, "loadingProgress": { "message": "加载进度" }, - "thisMightTakeFewMinutes": { - "message": "这可能需要几分钟时间。" + "reviewingMemberData": { + "message": "正在审查成员数据..." + }, + "analyzingPasswords": { + "message": "正在分析密码..." + }, + "calculatingRisks": { + "message": "正在计算风险..." + }, + "generatingReports": { + "message": "正在生成报告..." + }, + "compilingInsightsProgress": { + "message": "正在编译洞察..." + }, + "reportGenerationDone": { + "message": "完成!" }, "riskInsightsRunReport": { "message": "运行报告" @@ -5840,10 +5855,6 @@ "message": "不知道密码吗?请向发送者索取访问此 Send 所需的密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 默认隐藏。您可以使用下方的按钮切换其可见性。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下载附件" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受这些风险和策略更新" }, + "autoConfirmEnabledByAdmin": { + "message": "启用了自动用户确认设置" + }, + "autoConfirmDisabledByAdmin": { + "message": "停用了自动用户确认设置" + }, + "autoConfirmEnabledByPortal": { + "message": "添加了自动用户确认策略" + }, + "autoConfirmDisabledByPortal": { + "message": "禁用了自动用户确认策略" + }, + "system": { + "message": "系统" + }, "personalOwnership": { "message": "禁用个人密码库" }, @@ -6429,6 +6455,10 @@ "message": "查看 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "验证您的电子邮箱以查看此 Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "创建此 Send 的 Bitwarden 用户已选择隐藏他们的电子邮箱地址。在使用或下载其内容之前,您应确保信任此链接的来源。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6731,7 +6761,7 @@ "message": "1 份邀请未发送" }, "bulkReinviteFailureDescription": { - "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试重新发送,如果问题仍然存在,", + "message": "向 $TOTAL$ 位成员中的 $COUNT$ 位发送邀请时发生错误。请尝试再次发送,如果问题仍然存在,", "placeholders": { "count": { "content": "$1", @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "无效的验证码" }, + "invalidEmailOrVerificationCode": { + "message": "无效的电子邮箱或验证码" + }, "keyConnectorDomain": { "message": "Key Connector 域名" }, @@ -9138,7 +9171,7 @@ "message": "查看全部" }, "showingPortionOfTotal": { - "message": "显示 $PORTION$ / $TOTAL$", + "message": "显示 $TOTAL$ 中的 $PORTION$", "placeholders": { "portion": { "content": "$1", @@ -11896,13 +11929,10 @@ "noItemsInArchiveDesc": { "message": "已归档的项目将显示在此处,并将被排除在一般搜索结果和自动填充建议之外。" }, - "itemWasSentToArchive": { - "message": "项目已发送到归档" + "itemArchiveToast": { + "message": "项目已归档" }, - "itemWasUnarchived": { - "message": "项目已取消归档" - }, - "itemUnarchived": { + "itemUnarchivedToast": { "message": "项目已取消归档" }, "bulkArchiveItems": { @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "您已使用了全部的 $GB$ GB 加密存储空间。要继续存储文件,请添加更多存储空间。" }, + "extensionPromptHeading": { + "message": "获取扩展以便轻松访问密码库" + }, + "extensionPromptBody": { + "message": "安装浏览器扩展后,您可以随时随地在线使用 Bitwarden。它会自动填写密码,只需单击一下即可登录您的账户。" + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "跳过" + }, + "downloadExtension": { + "message": "下载扩展" + }, "whoCanView": { "message": "谁可以查看" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "输入多个电子邮箱(使用逗号分隔)。" }, + "emailsRequiredChangeAccessType": { + "message": "电子邮箱验证要求至少有一个电子邮箱地址。要移除所有电子邮箱,请更改上面的访问类型。" + }, "emailPlaceholder": { "message": "user@bitwarden.com, user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "无效的 Send 密码" }, + "vaultWelcomeDialogTitle": { + "message": "您已成功加入!欢迎使用 Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "将您的所有密码和个人信息存储在你的 Bitwarden 密码库中。我们将带您熟悉一下。" + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "开始导览" + }, + "vaultWelcomeDialogDismissCta": { + "message": "跳过" + }, "sendPasswordHelperText": { "message": "个人需要输入密码才能查看此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "更新您的付款方式时出错。" + }, + "sendPasswordInvalidAskOwner": { + "message": "无效的密码。请向发送者索取访问此 Send 所需的密码。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "此 Send 有效期至 $DATE$ $TIME$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 8056e49b08a..31099edc763 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -2803,7 +2803,7 @@ "message": "用戶端 ID" }, "twoFactorDuoClientSecret": { - "message": "用戶端秘密" + "message": "用戶端機密" }, "twoFactorDuoApiHostname": { "message": "API 主機名稱" @@ -3338,6 +3338,15 @@ "reinstated": { "message": "已重新開始訂閱。" }, + "resubscribe": { + "message": "Resubscribe" + }, + "yourSubscriptionIsExpired": { + "message": "Your subscription is expired" + }, + "yourSubscriptionIsCanceled": { + "message": "Your subscription is canceled" + }, "cancelConfirmation": { "message": "您確定要取消訂閱嗎?在本次計費週期結束後,您將無法再使用此訂閱的所有功能。" }, @@ -4328,6 +4337,15 @@ } } }, + "automaticallyConfirmedUserId": { + "message": "Automatically confirmed user $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, "editedUserId": { "message": "已編輯使用者 $ID$。", "placeholders": { @@ -4587,29 +4605,26 @@ "generatingYourAccessIntelligence": { "message": "正在產生您的 Access Intelligence……" }, - "fetchingMemberData": { - "message": "正在擷取成員資料…" - }, - "analyzingPasswordHealth": { - "message": "正在分析密碼安全狀況…" - }, - "calculatingRiskScores": { - "message": "正在計算風險分數…" - }, - "generatingReportData": { - "message": "正在產生報告資料..." - }, - "savingReport": { - "message": "正在儲存報告..." - }, - "compilingInsights": { - "message": "正在整理洞察結果…" - }, "loadingProgress": { "message": "載入進度中" }, - "thisMightTakeFewMinutes": { - "message": "這可能需要幾分鐘。" + "reviewingMemberData": { + "message": "Reviewing member data..." + }, + "analyzingPasswords": { + "message": "Analyzing passwords..." + }, + "calculatingRisks": { + "message": "Calculating risks..." + }, + "generatingReports": { + "message": "Generating reports..." + }, + "compilingInsightsProgress": { + "message": "Compiling insights..." + }, + "reportGenerationDone": { + "message": "Done!" }, "riskInsightsRunReport": { "message": "執行報告" @@ -5840,10 +5855,6 @@ "message": "不知道密碼?請向此 Send 的寄件者索取密碼。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendHiddenByDefault": { - "message": "此 Send 預設為隱藏。您可使用下方的按鈕切換其可見度。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "downloadAttachments": { "message": "下載附件" }, @@ -6136,6 +6147,21 @@ "autoConfirmCheckBoxLabel": { "message": "我接受這些風險與原則更新" }, + "autoConfirmEnabledByAdmin": { + "message": "Turned on Automatic user confirmation setting" + }, + "autoConfirmDisabledByAdmin": { + "message": "Turned off Automatic user confirmation setting" + }, + "autoConfirmEnabledByPortal": { + "message": "Added Automatic user confirmation policy" + }, + "autoConfirmDisabledByPortal": { + "message": "Removed Automatic user confirmation policy" + }, + "system": { + "message": "System" + }, "personalOwnership": { "message": "停用個人密碼庫" }, @@ -6429,6 +6455,10 @@ "message": "檢視 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "verifyYourEmailToViewThisSend": { + "message": "Verify your email to view this Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "viewSendHiddenEmailWarning": { "message": "建立此 Send 的 Bitwarden 使用者已選擇隱藏他們的電子郵件地址。在使用或下載此連結的內容之前,應確保您信任此連結的來源。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -7387,6 +7417,9 @@ "invalidVerificationCode": { "message": "無效的驗證碼" }, + "invalidEmailOrVerificationCode": { + "message": "Invalid email or verification code" + }, "keyConnectorDomain": { "message": "Key Connector 網域" }, @@ -9778,7 +9811,7 @@ } }, "secretsManagerForPlanDesc": { - "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理秘密資訊的功能。" + "message": "提供工程與 DevOps 團隊一套在軟體開發生命週期中,管理機密資訊的功能。" }, "free2PersonOrganization": { "message": "免費的 2 人組織" @@ -9820,7 +9853,7 @@ "message": "訂閲機密管理員" }, "addSecretsManagerUpgradeDesc": { - "message": "將機密管理員加入您的升級方案,來維持先前方案建立的秘密資訊的存取權限。" + "message": "將機密管理員加入您的升級方案,來維持先前方案建立的機密資訊的存取權限。" }, "additionalServiceAccounts": { "message": "額外服務帳戶" @@ -10893,7 +10926,7 @@ "message": "已驗證" }, "viewSecret": { - "message": "檢視秘密" + "message": "檢視機密" }, "noClients": { "message": "沒有可列出的客戶" @@ -11896,14 +11929,11 @@ "noItemsInArchiveDesc": { "message": "封存的項目會顯示在此處,且不會出現在一般搜尋結果或自動填入建議中。" }, - "itemWasSentToArchive": { - "message": "項目已移至封存" + "itemArchiveToast": { + "message": "項目已封存" }, - "itemWasUnarchived": { - "message": "已取消封存項目" - }, - "itemUnarchived": { - "message": "項目取消封存" + "itemUnarchivedToast": { + "message": "Item unarchived" }, "bulkArchiveItems": { "message": "項目已封存" @@ -12847,6 +12877,21 @@ "storageFullDescription": { "message": "您已用完全部 $GB$ GB 的加密儲存空間。如需繼續儲存檔案,請增加儲存空間。" }, + "extensionPromptHeading": { + "message": "Get the extension for easy vault access" + }, + "extensionPromptBody": { + "message": "With the browser extension installed, you'll take Bitwarden everywhere online. It'll fill in passwords, so you can log into your accounts with a single click." + }, + "extensionPromptImageAlt": { + "message": "A web browser showing the Bitwarden extension with autofill items for the current webpage." + }, + "skip": { + "message": "Skip" + }, + "downloadExtension": { + "message": "Download extension" + }, "whoCanView": { "message": "誰可以檢視" }, @@ -12859,6 +12904,9 @@ "enterMultipleEmailsSeparatedByComma": { "message": "請以逗號分隔輸入多個電子郵件地址。" }, + "emailsRequiredChangeAccessType": { + "message": "Email verification requires at least one email address. To remove all emails, change the access type above." + }, "emailPlaceholder": { "message": "user@bitwarden.com , user@acme.com" }, @@ -12883,6 +12931,18 @@ "invalidSendPassword": { "message": "Send 密碼無效" }, + "vaultWelcomeDialogTitle": { + "message": "You're in! Welcome to Bitwarden" + }, + "vaultWelcomeDialogDescription": { + "message": "Store all your passwords and personal info in your Bitwarden vault. We'll show you around." + }, + "vaultWelcomeDialogPrimaryCta": { + "message": "Start tour" + }, + "vaultWelcomeDialogDismissCta": { + "message": "Skip" + }, "sendPasswordHelperText": { "message": "對方必須輸入密碼才能檢視此 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -12938,5 +12998,23 @@ }, "paymentMethodUpdateError": { "message": "There was an error updating your payment method." + }, + "sendPasswordInvalidAskOwner": { + "message": "Invalid password. Ask the sender for the password needed to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendExpiresOn": { + "message": "This Send expires at $TIME$ on $DATE$", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "10:00 AM" + }, + "date": { + "content": "$2", + "example": "Jan 1, 1970" + } + } } } diff --git a/bitwarden_license/bit-common/src/dirt/docs/README.md b/bitwarden_license/bit-common/src/dirt/docs/README.md new file mode 100644 index 00000000000..f07f5c8b44c --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/README.md @@ -0,0 +1,73 @@ +# DIRT Team Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` +**Purpose:** Overview of DIRT team documentation with navigation to detailed guides + +--- + +## 🎯 Start Here + +**New to the DIRT team?** → [Getting Started](./getting-started.md) + +**Looking for something specific?** + +- **"What should I read for my task?"** → [Getting Started](./getting-started.md) +- **"How are docs organized?"** → [Documentation Structure](./documentation-structure.md) +- **"How do I implement a feature?"** → [Playbooks](./playbooks/) +- **"What are the coding standards?"** → [Standards](./standards/) +- **"How do services integrate with components?"** → [Integration Guide](./integration-guide.md) + +--- + +## 📁 What's in This Folder + +| Document/Folder | Purpose | +| -------------------------------------------------------------- | ------------------------------------------------- | +| **[getting-started.md](./getting-started.md)** | Navigation hub - what to read for your task | +| **[documentation-structure.md](./documentation-structure.md)** | Complete structure guide - how docs are organized | +| **[integration-guide.md](./integration-guide.md)** | Service ↔ Component integration patterns | +| **[playbooks/](./playbooks/)** | Step-by-step implementation guides | +| **[standards/](./standards/)** | Team coding and documentation standards | +| **[access-intelligence/](./access-intelligence/)** | Migration guides and architecture comparisons | + +--- + +## 🏗️ DIRT Team Features + +The DIRT team (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** - Organization security reporting and password health +- **Organization Integrations** - Third-party integrations +- **External Reports** - Organization reports (weak passwords, member access, etc.) +- **Phishing Detection** - Browser-based phishing detection + +**Documentation is organized by package:** + +- **bit-common** - Platform-agnostic services (work on all platforms) +- **bit-web** - Angular web components (web client only) +- **bit-browser** - Browser extension components + +For detailed feature documentation locations, see [Getting Started](./getting-started.md). + +--- + +## 📝 Creating New Documentation + +**Before creating new docs, follow these steps:** + +1. **Read the standards:** [Documentation Standards](./standards/documentation-standards.md) +2. **Check for overlaps:** Review existing docs to avoid duplication +3. **Follow the playbook:** [Documentation Playbook](./playbooks/documentation-playbook.md) +4. **Update navigation:** Add to [getting-started.md](./getting-started.md) if it's a primary entry point +5. **Update this README:** If adding a new category or top-level document + +**For detailed guidance on where to place docs, see:** + +- [Documentation Standards § Document Location Rules](./standards/documentation-standards.md#document-location-rules) +- [Documentation Structure](./documentation-structure.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md new file mode 100644 index 00000000000..7d7e20b5d31 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md @@ -0,0 +1,277 @@ +# DIRT Team Documentation Structure + +**Purpose:** Navigation guide for all DIRT team documentation organized by team/feature hierarchy + +--- + +## 📁 Documentation Organization + +DIRT team documentation follows a **team/feature** hierarchy organized across multiple locations based on separation of concerns: + +### Team-Level Documentation + +**Location:** `bitwarden_license/bit-common/src/dirt/docs/` + +**Scope:** Applies to all DIRT features (Access Intelligence, Phishing Detection, etc.) + +**Contains:** + +- Team playbooks (service, component, documentation) +- Team coding standards +- Integration guides +- Getting started guide + +### Feature-Level Documentation + +**Pattern:** Feature docs live **next to the feature code**, not in the team `docs/` folder. + +**Location:** `dirt/[feature]/docs/` + +**Examples:** + +- **Access Intelligence:** `dirt/access-intelligence/v2/docs/` (or `dirt/access-intelligence/docs/` for current version) +- **Phishing Detection (future):** `dirt/phishing-detection/docs/` + +**Feature docs contain:** + +- Feature-specific architecture +- Feature-specific implementation guides +- Feature-specific patterns + +**Exception:** Migration/transition documentation can live in team `docs/` as **team-level knowledge**. Example: `docs/access-intelligence/` contains migration guides from v1 to v2, which is team-level context about the transition, not feature-specific architecture. + +### 1. Services & Architecture (Platform-Agnostic) + +**Pattern:** `bitwarden_license/bit-common/src/dirt/[feature]/docs/` + +**Purpose:** Feature-specific documentation lives next to the feature code + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/v2/docs/` (for v2 architecture) +- Contains: Architecture docs, implementation guides specific to that version + +**Note:** Team-level migration docs may live in `docs/access-intelligence/` as team knowledge about the transition between versions. + +### 2. Components (Angular-Specific) + +**Pattern:** `bitwarden_license/bit-web/src/app/dirt/[feature]/docs/` + +**Purpose:** Angular-specific UI components for web client only + +**Example for Access Intelligence:** + +- Location: `dirt/access-intelligence/docs/` +- Contains: Component inventory, migration guides, Storybook + +--- + +## 🎯 Where to Start? + +**For navigation guidance (what to read), see:** [getting-started.md](./getting-started.md) + +This document focuses on **how** the documentation is organized, not **what** to read. + +--- + +## 🗂️ Complete File Structure + +``` +# ============================================================================ +# SERVICES & ARCHITECTURE (bit-common) +# Platform-agnostic - Used by web, desktop, browser, CLI +# ============================================================================ + +bitwarden_license/bit-common/src/dirt/ +├── docs/ ← TEAM-LEVEL documentation only +│ ├── README.md ← Team docs overview +│ ├── getting-started.md ← Entry point for team +│ ├── documentation-structure.md ← This file +│ ├── integration-guide.md ← Service ↔ Component integration +│ │ +│ ├── playbooks/ ← Team playbooks (service, component, docs) +│ │ └── README.md ← Playbook navigation +│ │ +│ ├── standards/ ← Team coding standards +│ │ └── standards.md ← Core standards +│ │ +│ └── access-intelligence/ ← EXCEPTION: Migration guides (team knowledge) +│ ├── README.md ← Migration overview +│ ├── ... ← Migration analysis files +│ ├── architecture/ ← Migration architecture comparison +│ │ └── ... ← Architecture comparison files +│ └── implementation/ ← Implementation guides +│ └── ... ← Integration guides +│ +└── [feature]/ ← FEATURE CODE + FEATURE DOCS + └── docs/ ← Feature-specific documentation + ├── README.md ← Feature docs navigation + ├── architecture/ ← Feature architecture (lives with code) + │ └── ... ← Architecture files + └── implementation/ ← Feature implementation guides + └── ... ← Implementation guide files + +# Example for Access Intelligence v2: +bitwarden_license/bit-common/src/dirt/access-intelligence/ +├── v2/ ← V2 implementation +│ ├── services/ ← V2 services +│ ├── models/ ← V2 models +│ └── docs/ ← V2-SPECIFIC documentation +│ ├── README.md ← V2 docs overview +│ ├── architecture/ ← V2 architecture +│ │ └── ... ← Architecture files +│ └── implementation/ ← V2 implementation guides +│ └── ... ← Implementation guide files +└── v1/ ← V1 implementation (legacy) + +# ============================================================================ +# COMPONENTS (bit-web) +# Angular-specific - Web client only +# ============================================================================ + +bitwarden_license/bit-web/src/app/dirt/[feature]/ +├── docs/ ← Component documentation +│ └── README.md ← Component docs navigation +├── [component folders]/ ← Angular components +└── v2/ ← V2 components (if applicable) + +# Example for Access Intelligence: +bitwarden_license/bit-web/src/app/dirt/access-intelligence/ +├── docs/ ← Component documentation +│ ├── README.md ← Component docs navigation +│ └── ... ← Component guides +├── [components]/ ← Angular components +└── v2/ ← V2 components (if applicable) + └── ... ← V2 component files +``` + +--- + +## 🔄 When to Update This Structure + +Update this document when: + +- [ ] Adding new documentation categories +- [ ] Changing file locations +- [ ] Restructuring documentation organization + +--- + +## 📝 Architecture Decisions + +**Where decisions are tracked:** + +- **Company-wide ADRs:** Stored in the `contributing-docs` repository +- **Feature-specific decisions:** Tracked in Confluence (link to be added) +- **Local decision notes (optional):** `~/Documents/bitwarden-notes/dirt/decisions/[feature]/` for personal reference before moving to Confluence + - Example: `~/Documents/bitwarden-notes/dirt/decisions/access-intelligence/` + +**What goes in repo architecture docs:** + +- Current architecture state +- Migration plans and roadmaps +- Technical constraints +- Implementation patterns + +**What goes in Confluence:** + +- Decision discussions and rationale +- Alternative approaches considered +- Stakeholder input +- Links to Slack discussions + +--- + +## ✏️ Creating New Documentation + +**Before creating new documentation, see:** [docs/README.md](./README.md) § Documentation Best Practices + +**Key principles:** + +- **Single responsibility** - Each document should answer one question +- **Check for overlaps** - Read related docs first +- **Follow naming conventions** - See [documentation-standards.md](./standards/documentation-standards.md) +- **Cross-reference standards** - See [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- **Update navigation** - Add to getting-started.md if it's a primary entry point + +--- + +## 📊 Why This Structure? + +### Documentation Placement Principles + +**Team-Level Documentation (`docs/`):** + +- Applies to all DIRT features +- Playbooks, standards, getting-started guides +- Migration guides and transition documentation (team knowledge about rewrites) +- Cross-feature integration patterns + +**Feature-Level Documentation (`dirt/[feature]/docs/`):** + +- Lives **next to the feature code** +- Feature-specific architecture +- Version-specific implementation details +- Feature-specific patterns + +**Rationale:** + +- **Discoverability:** Architecture docs are found where the code lives +- **Versioning:** v1 and v2 can have separate docs directories +- **Maintainability:** Update feature docs without touching team docs +- **Clarity:** Clear separation between "what applies to all features" vs "what applies to this feature" + +### Separation of Concerns + +**Platform-Agnostic (bit-common):** + +- Services work on all platforms (web, desktop, browser, CLI) +- Domain models are platform-independent +- Architecture decisions affect all clients +- **Feature docs live with feature code:** `dirt/[feature]/docs/` + +**Angular-Specific (bit-web):** + +- Components only used in web client +- Storybook is web-only +- Angular-specific patterns (OnPush, Signals, etc.) +- **Component docs live with components:** `dirt/[feature]/docs/` + +### Benefits + +1. **Clarity:** Developers know where to look based on what they're working on +2. **Separation:** Team docs vs feature docs, Angular code vs platform-agnostic code +3. **Discoverability:** Feature docs are near feature code +4. **Maintainability:** Easier to update feature docs without affecting team docs +5. **Scalability:** Can add versioned docs (v1/, v2/) next to versioned code +6. **Migration clarity:** Team `docs/` can hold migration guides while feature `docs/` hold version-specific architecture + +--- + +## 🆘 Need Help? + +### Can't Find Documentation? + +1. **Start with getting-started.md:** [getting-started.md](./getting-started.md) + - Navigation hub for all DIRT team documentation + - Links to all major documentation categories + +2. **Check README files:** + - [Team Documentation README](./README.md) + - [Component README](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +3. **Check feature-specific docs:** + - Look in `dirt/[feature]/docs/` next to the feature code + - Example: `dirt/access-intelligence/v2/docs/` + +### Links Broken? + +- Check if file was moved +- Update cross-references following [documentation-standards.md § Cross-Reference Standards](./standards/documentation-standards.md#cross-reference-standards) +- Update navigation in README.md files + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/getting-started.md b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md new file mode 100644 index 00000000000..0077019fe02 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/getting-started.md @@ -0,0 +1,96 @@ +# DIRT Team - Getting Started + +**Purpose:** Navigation hub showing what documentation is available for your work + +--- + +## 🎯 DIRT Team Features + +The **DIRT team** (Data, Insights, Reporting & Tooling) owns: + +- **Access Intelligence** (formerly Risk Insights) + - Organization security reporting and password health analysis + - Location: `dirt/reports/risk-insights/` (v1 services), `bit-web/.../access-intelligence/` (UI) + - Note: `risk-insights` is the v1 codebase name for Access Intelligence + +- **Organization Integrations** + - Third-party organization integrations + - Location: `dirt/organization-integrations/` + +- **External Reports** + - Various organization reports (weak password report, member access report, etc.) + - Documentation: Coming soon + +- **Phishing Detection** + - Documentation: Coming soon + +**Note:** Access Intelligence has the most documentation as it's the first feature we're documenting comprehensively. + +--- + +## 📚 What's Available + +### Development Resources + +| Resource Type | What It Provides | Where to Find It | +| ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Playbooks** | Step-by-step implementation guides for common dev tasks | [Playbooks Hub](./playbooks/) | +| **Standards** | Coding conventions, patterns, and best practices | [Standards Hub](./standards/README.md) | +| **Architecture** | Feature architecture reviews and migration plans | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Integration Guides** | How services and components work together | [Generic Guide](./integration-guide.md), [Access Intelligence](./access-intelligence/service-component-integration.md) | +| **Documentation Guide** | How docs are organized and where to find things | [Documentation Structure](./documentation-structure.md) | + +### Standards by Area + +| Area | Standard Document | +| ---------------------- | -------------------------------------------------------------------------- | +| **General Coding** | [Standards Hub](./standards/README.md) | +| **Services** | [Service Standards](./standards/service-standards.md) | +| **Domain Models** | [Model Standards](./standards/model-standards.md) | +| **Service Testing** | [Service Testing Standards](./standards/testing-standards-services.md) | +| **Angular Components** | [Angular Standards](./standards/angular-standards.md) | +| **Component Testing** | [Component Testing Standards](./standards/testing-standards-components.md) | +| **RxJS Patterns** | [RxJS Standards](./standards/rxjs-standards.md) | +| **Code Organization** | [Code Organization Standards](./standards/code-organization-standards.md) | +| **Documentation** | [Documentation Standards](./standards/documentation-standards.md) | + +### Playbooks by Task + +| Task | Playbook | +| ------------------------------------ | --------------------------------------------------------------------------------- | +| **Implement or refactor a service** | [Service Implementation Playbook](./playbooks/service-implementation-playbook.md) | +| **Migrate or create a UI component** | [Component Migration Playbook](./playbooks/component-migration-playbook.md) | +| **Create or update documentation** | [Documentation Playbook](./playbooks/documentation-playbook.md) | +| **Browse all playbooks** | [Playbooks Hub](./playbooks/) | + +--- + +## 🚀 Quick Reference by Task + +| What are you working on? | Start here | +| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Services** (implementation, architecture, testing) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Standards](./standards/service-standards.md) | +| **Domain Models** (view models, query methods) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Model Standards](./standards/model-standards.md) | +| **UI Components** (Angular, migration, testing) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Angular Standards](./standards/angular-standards.md) | +| **Storybook** (create or update stories) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards § Storybook](./standards/testing-standards-components.md#storybook-as-living-documentation) | +| **Component Tests** (Jest, OnPush, Signals) | [Component Playbook](./playbooks/component-migration-playbook.md) + [Component Testing Standards](./standards/testing-standards-components.md) | +| **Service Tests** (mocks, observables, RxJS) | [Service Playbook](./playbooks/service-implementation-playbook.md) + [Service Testing Standards](./standards/testing-standards-services.md) | +| **Documentation** (create, update, organize) | [Documentation Playbook](./playbooks/documentation-playbook.md) + [Documentation Standards](./standards/documentation-standards.md) | +| **Architecture Review** (feature planning) | [Access Intelligence Architecture](./access-intelligence/architecture/) | +| **Feature Architecture Decisions** | Document in [docs/[feature]/architecture/](./documentation-structure.md#feature-level-documentation) (decisions tracked in Confluence) | + +--- + +## 🆘 Need Help? + +**Can't find what you're looking for?** + +- **Understand how docs are organized:** See [Documentation Structure](./documentation-structure.md) +- **Browse all team documentation:** See [Team Docs README](./README.md) +- **Component-specific docs:** See [Component Docs](/bitwarden_license/bit-web/src/app/dirt/access-intelligence/docs/README.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md new file mode 100644 index 00000000000..0d7bf3db847 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/docs/integration-guide.md @@ -0,0 +1,378 @@ +# Service ↔ Component Integration Guide + +**Purpose:** Coordination guide for features that span both platform-agnostic services (bit-common) and Angular UI components (bit-web/bit-browser) + +**Scope:** This guide applies to **any DIRT feature** requiring work in both service and component layers. For feature-specific integration patterns and detailed examples, see the feature's documentation: + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +**Focus:** This document focuses on **coordination and handoffs** between service and component developers. For code patterns and standards, see [Standards Documentation](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md). + +--- + +## 📋 When You Need Both + +Many DIRT features require coordinated work across service AND component layers: + +| Feature Type | Service Work | Component Work | +| -------------------------- | ----------------------------- | --------------------------------- | +| **New report/data type** | Generate, persist, load data | Display data, filters, navigation | +| **New data visualization** | Aggregate/query data | Charts, cards, tables | +| **User actions** | Business logic on models | UI interactions, forms | +| **Settings/preferences** | Persist settings | Settings UI | +| **Integrations** | API communication, sync logic | Configuration UI, status display | + +--- + +## 🔄 Integration Pattern + +``` +┌─────────────────────────────────────────────────┐ +│ Component (bit-web/bit-browser) │ +│ - User interactions │ +│ - Display logic │ +│ - Converts Observables → Signals (toSignal()) │ +│ - OnPush + Signal inputs/outputs │ +├─────────────────────────────────────────────────┤ +│ Data Service (Feature-specific) │ +│ - Exposes Observable streams │ +│ - Coordinates feature data │ +│ - Delegates business logic to models │ +│ - Delegates persistence to services │ +├─────────────────────────────────────────────────┤ +│ Domain Services (bit-common) │ +│ - Business logic orchestration │ +│ - Pure transformation │ +│ - Platform-agnostic │ +├─────────────────────────────────────────────────┤ +│ View Models │ +│ - Smart models (CipherView pattern) │ +│ - Query methods: getData(), filter(), etc. │ +│ - Mutation methods: update(), delete(), etc. │ +└─────────────────────────────────────────────────┘ +``` + +**Key principle:** Services do the work, components coordinate the UI. Business logic lives in view models, not components. + +--- + +## 🔀 Service → Component Handoff + +**When:** Service implementation is complete, ready for UI integration + +### Readiness Checklist + +Before handing off to component developer, ensure: + +- [ ] **Service is complete and tested** + - [ ] Abstract defined with JSDoc + - [ ] Implementation complete + - [ ] Tests passing (`npm run test`) + - [ ] Types validated (`npm run test:types`) + +- [ ] **View models have required methods** + - [ ] Query methods for component data needs (documented) + - [ ] Mutation methods for user actions (documented) + - [ ] Methods follow naming conventions + +- [ ] **Data service exposes observables** + - [ ] Observable(s) are public and documented + - [ ] Observable emits correct view models + - [ ] Observable handles errors gracefully + +- [ ] **Component requirements documented** + - [ ] What data the component needs + - [ ] What user actions the component handles + - [ ] What the component should display + - [ ] Any performance considerations + +### Handoff Communication Template + +When handing off to component developer, provide: + +1. **What service to inject** + - Example: `FeatureDataService` + +2. **What observable(s) to use** + - Example: `data$: Observable` + - Type signature and nullability + +3. **What model methods are available** + - Query methods: `feature.getData()`, `feature.filter(criteria)` + - Mutation methods: `feature.update(data)`, `feature.delete(id)` + - Link to model documentation or JSDoc + +4. **How to integrate in component** + - Reference [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + - Basic pattern: inject service → convert observable to signal → use in template + +5. **Any gotchas or special considerations** + - Performance notes (large datasets, expensive operations) + - Error handling requirements + - Special states (loading, empty, error) + +### Communication Methods + +- **Slack:** Quick handoff for simple integrations +- **Jira comment:** Document handoff details on feature ticket +- **Documentation:** Update feature docs with integration examples +- **Pair session:** For complex integrations, schedule pairing + +--- + +## 🔀 Component → Service Handoff + +**When:** Component needs new data/functionality not yet available in services + +### Discovery Checklist + +Before creating a service request, identify: + +- [ ] **What's missing** + - [ ] New query method needed on view model? + - [ ] New mutation method needed on view model? + - [ ] New service needed entirely? + - [ ] New data needs to be loaded/persisted? + +- [ ] **Document the requirement clearly** + - [ ] What data the component needs (shape, type) + - [ ] What format the data should be in + - [ ] What user action triggers this need + - [ ] Performance requirements (dataset size, frequency) + +- [ ] **Assess scope** + - [ ] Is this a new method on existing model? (small change) + - [ ] Is this a new service? (medium-large change) + - [ ] Does this require API changes? (involves backend team) + +- [ ] **File appropriate ticket** + - [ ] Link to component/feature that needs it + - [ ] Link to design/mockup if applicable + - [ ] Tag service developer or tech lead + +### Handoff Communication Template + +When requesting service work, provide: + +1. **What the component needs** + - Clear description: "Component needs list of filtered items based on user criteria" + +2. **Proposed API (if you have one)** + - Example: `model.getFilteredItems(criteria): Item[]` + - This is negotiable, service developer may suggest better approach + +3. **Why (user story/context)** + - Example: "User clicks 'Show only critical' filter, UI should update to show subset" + +4. **Data format expected** + - Example: "Array of `{ id: string, name: string, isCritical: boolean }`" + - Or reference existing model type if reusing + +5. **Performance/scale considerations** + - Example: "Could be 1000+ items for large organizations" + - Helps service developer optimize + +6. **Timeline/priority** + - Is this blocking component work? + - Can component proceed with stub/mock for now? + +### Communication Methods + +- **Jira ticket:** For non-trivial work requiring tracking +- **Slack:** For quick questions or small additions +- **Planning session:** For large features requiring design discussion +- **ADR:** If architectural decision needed + +--- + +## 🤝 Collaboration Patterns + +### Pattern 1: Parallel Development + +**When to use:** Service and component work can be developed simultaneously + +**How:** + +1. Service developer creates interface/abstract first +2. Component developer uses interface with mock data +3. Both develop in parallel +4. Integration happens at the end + +**Benefits:** Faster delivery, clear contracts + +### Pattern 2: Sequential Development (Service First) + +**When to use:** Component needs complete service implementation + +**How:** + +1. Service developer implements fully +2. Service developer documents integration +3. Component developer integrates +4. Component developer provides feedback + +**Benefits:** Fewer integration issues, clearer requirements + +### Pattern 3: Sequential Development (Component First) + +**When to use:** UI/UX needs to be proven before service investment + +**How:** + +1. Component developer builds with mock data +2. Component developer documents data needs +3. Service developer implements to match needs +4. Integration and refinement + +**Benefits:** User-driven design, avoids unused service work + +### Pattern 4: Paired Development + +**When to use:** Complex integration, unclear requirements, new patterns + +**How:** + +1. Service and component developer pair on design +2. Develop together or in short iterations +3. Continuous feedback and adjustment + +**Benefits:** Fastest problem solving, shared understanding + +--- + +## 🧪 Testing Integration Points + +### Service Layer Testing + +**Service developers should test:** + +- Services return correct view models +- Observables emit expected data +- Error handling works correctly +- Performance is acceptable for expected dataset sizes + +**Reference:** [Service Implementation Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) + +### Component Layer Testing + +**Component developers should test:** + +- Services are correctly injected +- Observables are correctly converted to signals +- View model methods are called appropriately +- Data is displayed correctly +- User interactions trigger correct model methods + +**Reference:** [Component Migration Playbook - Testing](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Integration Testing + +**Both should coordinate on:** + +- Full user flows work end-to-end +- Data flows correctly from service → component +- UI updates when data changes +- Error states are handled gracefully + +--- + +## 🚨 Common Integration Pitfalls + +### 1. Component Bypasses Data Service + +**Problem:** Component directly calls API services or persistence layers + +**Why it's bad:** Breaks abstraction, duplicates logic, harder to test + +**Solution:** Always go through feature's data service layer + +**Reference:** [Standards: Service Layer Pattern](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 2. Service Returns Plain Objects + +**Problem:** Service returns `{ ... }` instead of view model instances + +**Why it's bad:** Loses model methods, breaks encapsulation, business logic leaks to components + +**Solution:** Always return view model instances with query/mutation methods + +**Reference:** [Standards: View Models](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 3. Business Logic in Components + +**Problem:** Component implements filtering, calculations, state changes + +**Why it's bad:** Logic not reusable, harder to test, violates separation of concerns + +**Solution:** Business logic belongs in view models or domain services + +**Reference:** [Standards: Component Responsibilities](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 4. Manual Observable Subscriptions + +**Problem:** Component uses `.subscribe()` instead of `toSignal()` + +**Why it's bad:** Memory leaks, manual cleanup needed, doesn't leverage Angular signals + +**Solution:** Use `toSignal()` for automatic cleanup and signal integration + +**Reference:** [Standards: Observable to Signal Conversion](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) + +### 5. Unclear Handoff + +**Problem:** Service developer finishes work but doesn't communicate to component developer + +**Why it's bad:** Delays integration, component developer doesn't know work is ready + +**Solution:** Use handoff communication templates above, update Jira tickets, notify in Slack + +--- + +## 📞 Who to Contact + +### Service Questions + +- Check: [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team service developers + +### Component Questions + +- Check: [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) +- Check: [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- Ask: DIRT team component developers + +### Architecture Questions + +- Check: [Architecture Docs](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/architecture/) +- Check: [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- Ask: DIRT team tech lead + +### Coordination/Process Questions + +- Ask: DIRT team lead or scrum master + +--- + +## 📚 Related Documentation + +### General Guides + +- [Getting Started](/bitwarden_license/bit-common/src/dirt/docs/getting-started.md) +- [Standards](/bitwarden_license/bit-common/src/dirt/docs/standards/standards.md) +- [Documentation Structure](/bitwarden_license/bit-common/src/dirt/docs/documentation-structure.md) + +### Implementation Playbooks + +- [Service Implementation Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/service-implementation-playbook.md) +- [Component Migration Playbook](/bitwarden_license/bit-common/src/dirt/docs/playbooks/component-migration-playbook.md) + +### Feature-Specific Integration Guides + +- [Access Intelligence Integration](/bitwarden_license/bit-common/src/dirt/docs/access-intelligence/service-component-integration.md) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-02-17 +**Maintainer:** DIRT Team diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts index b5816ba34a2..5f642dd6503 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/datadog-template.ts @@ -14,19 +14,27 @@ export class DatadogTemplate implements OrgIntegrationTemplate { ddsource: "bitwarden", service: "event-logs", event: { - service: "payments", object: "event", - type: "#Type#", + type: "#TypeId#", + typeName: "#Type#", + memberId: "#UserId#", + organizationId: "#OrganizationId#", + providerId: "#ProviderId#", itemId: "#CipherId#", collectionId: "#CollectionId#", groupId: "#GroupId#", policyId: "#PolicyId#", - memberId: "#UserId#", + organizationUserId: "#OrganizationUserId#", + providerUserId: "#ProviderUserId#", + providerOrganizationId: "#ProviderOrganizationId#", actingUserId: "#ActingUserId#", installationId: "#InstallationId#", date: "#DateIso8601#", - device: "#DeviceType#", + deviceType: "#DeviceType#", + device: "#DeviceTypeId#", ipAddress: "#IpAddress#", + systemUser: "#SystemUser#", + domainName: "#DomainName#", secretId: "#SecretId#", projectId: "#ProjectId#", serviceAccountId: "#ServiceAccountId#", diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template.ts index 3c0cf3b9b35..2da8624c66f 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template.ts @@ -17,27 +17,29 @@ export class HecTemplate implements OrgIntegrationTemplate { service: "event-logs", event: { object: "event", - type: "#Type#", + type: "#TypeId#", + typeName: "#Type#", + memberId: "#UserId#", + organizationId: "#OrganizationId#", + providerId: "#ProviderId#", itemId: "#CipherId#", collectionId: "#CollectionId#", groupId: "#GroupId#", policyId: "#PolicyId#", - memberId: "#UserId#", + organizationUserId: "#OrganizationUserId#", + providerUserId: "#ProviderUserId#", + providerOrganizationId: "#ProviderOrganizationId#", actingUserId: "#ActingUserId#", installationId: "#InstallationId#", date: "#DateIso8601#", - device: "#DeviceType#", + deviceType: "#DeviceType#", + device: "#DeviceTypeId#", ipAddress: "#IpAddress#", + systemUser: "#SystemUser#", + domainName: "#DomainName#", secretId: "#SecretId#", projectId: "#ProjectId#", serviceAccountId: "#ServiceAccountId#", - actingUserName: "#ActingUserName#", - actingUserEmail: "#ActingUserEmail#", - actingUserType: "#ActingUserType#", - userName: "#UserName#", - userEmail: "#UserEmail#", - userType: "#UserType#", - groupName: "#GroupName#", }, }; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.spec.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.spec.ts index 767c22e2014..8c4bcbec24e 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.spec.ts @@ -9,6 +9,7 @@ import { } from "@bitwarden/common/types/guid"; import { OrgIntegrationBuilder } from "../models/integration-builder"; +import { OrganizationIntegration } from "../models/organization-integration"; import { OrganizationIntegrationConfigurationRequest } from "../models/organization-integration-configuration-request"; import { OrganizationIntegrationConfigurationResponse } from "../models/organization-integration-configuration-response"; import { OrganizationIntegrationRequest } from "../models/organization-integration-request"; @@ -207,7 +208,11 @@ describe("OrganizationIntegrationService", () => { const result = await service.save(orgId, OrganizationIntegrationType.Hec, config, template); - expect(result).toEqual({ mustBeOwner: false, success: true }); + expect(result).toEqual({ + mustBeOwner: false, + success: true, + organizationIntegrationResult: expect.any(OrganizationIntegration), + }); expect(integrationApiService.createOrganizationIntegration).toHaveBeenCalledWith( orgId, expect.any(OrganizationIntegrationRequest), @@ -325,7 +330,11 @@ describe("OrganizationIntegrationService", () => { template, ); - expect(result).toEqual({ mustBeOwner: false, success: true }); + expect(result).toEqual({ + mustBeOwner: false, + success: true, + organizationIntegrationResult: expect.any(OrganizationIntegration), + }); expect(integrationApiService.updateOrganizationIntegration).toHaveBeenCalledWith( orgId, integrationId, @@ -375,7 +384,11 @@ describe("OrganizationIntegrationService", () => { template, ); - expect(result).toEqual({ mustBeOwner: true, success: false }); + expect(result).toEqual({ + mustBeOwner: true, + success: false, + organizationIntegrationResult: undefined, + }); }); it("should rethrow non-404 errors", async () => { diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.ts index c9457f4bcfc..355230d07f7 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/organization-integration-service.ts @@ -29,6 +29,7 @@ import { OrganizationIntegrationConfigurationApiService } from "./organization-i export type IntegrationModificationResult = { mustBeOwner: boolean; success: boolean; + organizationIntegrationResult?: OrganizationIntegration | undefined; }; /** @@ -113,10 +114,14 @@ export class OrganizationIntegrationService { if (newIntegration !== null) { this._integrations$.next([...this._integrations$.getValue(), newIntegration]); } - return { mustBeOwner: false, success: true }; + return { + mustBeOwner: false, + success: newIntegration !== null, + organizationIntegrationResult: newIntegration ?? undefined, + }; } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 404) { - return { mustBeOwner: true, success: false }; + return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined }; } throw error; } @@ -178,10 +183,14 @@ export class OrganizationIntegrationService { } this._integrations$.next([...integrations]); } - return { mustBeOwner: false, success: true }; + return { + mustBeOwner: false, + success: updatedIntegration !== null, + organizationIntegrationResult: updatedIntegration ?? undefined, + }; } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 404) { - return { mustBeOwner: true, success: false }; + return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined }; } throw error; } @@ -221,10 +230,10 @@ export class OrganizationIntegrationService { .filter((i) => i.id !== integrationId); this._integrations$.next(updatedIntegrations); - return { mustBeOwner: false, success: true }; + return { mustBeOwner: false, success: true, organizationIntegrationResult: undefined }; } catch (error) { if (error instanceof ErrorResponse && error.statusCode === 404) { - return { mustBeOwner: true, success: false }; + return { mustBeOwner: true, success: false, organizationIntegrationResult: undefined }; } throw error; } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/shared/filter-integrations.pipe.ts similarity index 81% rename from bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts rename to bitwarden_license/bit-common/src/dirt/organization-integrations/shared/filter-integrations.pipe.ts index 10ee251a921..55eccd8cfca 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/shared/filter-integrations.pipe.ts @@ -1,10 +1,12 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { IntegrationType } from "@bitwarden/common/enums"; +import { Integration } from "../models/integration"; + @Pipe({ name: "filterIntegrations", + standalone: true, }) export class FilterIntegrationsPipe implements PipeTransform { transform(integrations: Integration[] | null | undefined, type: IntegrationType): Integration[] { diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/shared/integration-state.service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/shared/integration-state.service.ts new file mode 100644 index 00000000000..eb94ef66f1a --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/shared/integration-state.service.ts @@ -0,0 +1,18 @@ +import { Signal } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; + +import { Integration } from "../models/integration"; +import { OrganizationIntegration } from "../models/organization-integration"; + +export abstract class IntegrationStateService { + abstract integrations: Signal; + abstract organization: Signal; + abstract setIntegrations(integrations: Integration[]): void; + abstract setOrganization(organization: Organization | undefined): void; + abstract updateIntegrationSettings( + integrationName: string, + updatedIntegrationSettings: OrganizationIntegration, + ): void; + abstract deleteIntegrationSettings(integrationName: string): void; +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html index 070505a53b2..c79b39a6feb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.html @@ -50,56 +50,64 @@
- - - {{ "loading" | i18n }} - - -

{{ "noEventsInList" | i18n }}

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

{{ "noEventsInList" | i18n }}

+ } + + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "device" | i18n }} + {{ "user" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + + {{ e.appName }}, {{ e.ip }} + + + {{ e.userName }} + + + + } + + + } + @if (continuationToken) { + + } +
+} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 3d00d897175..fe14e56bbaa 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -94,7 +94,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit { this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); }); await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index abdd35c5e61..0a3b78bb014 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -6,6 +6,7 @@ import { FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components"; import { MemberActionsService } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-actions/member-actions.service"; +import { MemberDialogManagerService } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; import { @@ -83,6 +84,11 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr VerifyRecoverDeleteProviderComponent, SetupBusinessUnitComponent, ], - providers: [WebProviderService, ProviderActionsService, MemberActionsService], + providers: [ + WebProviderService, + ProviderActionsService, + MemberActionsService, + MemberDialogManagerService, + ], }) export class ProvidersModule {} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts index 5b9cea436a0..13018ba6884 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts @@ -9,8 +9,8 @@ import { Signal, signal, } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { catchError, EMPTY, from, switchMap, take } from "rxjs"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { firstValueFrom } from "rxjs"; import { ApplicationHealthReportDetail, @@ -238,6 +238,12 @@ export class NewApplicationsDialogComponent { // Checks if there are selected applications and proceeds to assign tasks async handleMarkAsCritical() { + if (this.markingAsCritical()) { + return; // Prevent double-click + } + + this.markingAsCritical.set(true); + if (this.selectedApplications().size === 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "confirmNoSelectedCriticalApplicationsTitle" }, @@ -246,25 +252,11 @@ export class NewApplicationsDialogComponent { }); if (!confirmed) { + this.markingAsCritical.set(false); return; } } - // Skip the assign tasks view if there are no new unassigned at-risk cipher IDs - if (this.newUnassignedAtRiskCipherIds().length === 0) { - this.handleAssignTasks(); - } else { - this.currentView.set(DialogView.AssignTasks); - } - } - - // Saves the application review and assigns tasks for unassigned at-risk ciphers - protected handleAssignTasks() { - if (this.saving()) { - return; // Prevent double-click - } - this.saving.set(true); - const reviewedDate = new Date(); const updatedApplications = this.dialogParams.newApplications.map((app) => { const isCritical = this.selectedApplications().has(app.applicationName); @@ -276,56 +268,79 @@ export class NewApplicationsDialogComponent { }); // Save the application review dates and critical markings - this.dataService - .saveApplicationReviewStatus(updatedApplications) - .pipe( - takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule - take(1), - switchMap(() => { - // Assign password change tasks for unassigned at-risk ciphers for critical applications - return from( - this.securityTasksService.requestPasswordChangeForCriticalApplications( - this.dialogParams.organizationId, - this.newUnassignedAtRiskCipherIds(), - ), - ); - }), - catchError((error: unknown) => { - if (error instanceof ErrorResponse && error.statusCode === 404) { - this.toastService.showToast({ - message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), - variant: "error", - title: this.i18nService.t("error"), - }); + try { + await firstValueFrom(this.dataService.saveApplicationReviewStatus(updatedApplications)); - this.saving.set(false); - return EMPTY; - } - - this.logService.error( - "[NewApplicationsDialog] Failed to save application review or assign tasks", - error, - ); - this.saving.set(false); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorSavingReviewStatus"), - message: this.i18nService.t("pleaseTryAgain"), - }); - - this.saving.set(false); - return EMPTY; - }), - ) - .subscribe(() => { - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("applicationReviewSaved"), - message: this.i18nService.t("newApplicationsReviewed"), - }); - this.saving.set(false); - this.handleAssigningCompleted(); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("applicationReviewSaved"), + message: this.i18nService.t("newApplicationsReviewed"), }); + + // If there are no unassigned at-risk ciphers, we can complete immediately. Otherwise, navigate to the assign tasks view. + if (this.newUnassignedAtRiskCipherIds().length === 0) { + this.handleAssigningCompleted(); + } else { + this.currentView.set(DialogView.AssignTasks); + } + } catch (error: unknown) { + this.logService.error( + "[NewApplicationsDialog] Failed to save application review status", + error, + ); + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorSavingReviewStatus"), + message: this.i18nService.t("pleaseTryAgain"), + }); + } finally { + this.markingAsCritical.set(false); + } + } + + // Saves the application review and assigns tasks for unassigned at-risk ciphers + protected async handleAssignTasks() { + if (this.saving()) { + return; // Prevent double-click + } + this.saving.set(true); + + try { + await this.securityTasksService.requestPasswordChangeForCriticalApplications( + this.dialogParams.organizationId, + this.newUnassignedAtRiskCipherIds(), + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("success"), + message: this.i18nService.t("notifiedMembers"), + }); + + // close the dialog + this.handleAssigningCompleted(); + } catch (error: unknown) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + this.toastService.showToast({ + message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), + variant: "error", + title: this.i18nService.t("error"), + }); + + return; + } + + this.logService.error("[NewApplicationsDialog] Failed to assign tasks", error); + + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } finally { + this.saving.set(false); + } } /** diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html index 27864fa2f87..ec73c4f47e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.html @@ -22,7 +22,7 @@
- @if (selectedUrls().size > 0) { + @if (visibleSelectedApps().size > 0) { @if (allSelectedAppsAreCritical()) { } @else { } } @@ -79,14 +79,15 @@ [dataSource]="dataSource" [selectedUrls]="selectedUrls()" [openApplication]="drawerDetails.invokerId || ''" - [checkboxChange]="onCheckboxChange" [showAppAtRiskMembers]="showAppAtRiskMembers" + (checkboxChange)="onCheckboxChange($event)" + (selectAllChange)="onSelectAllChange($event)" class="tw-mb-10" > - @if (emptyTableExplanation()) { + @if (this.dataSource.filteredData?.length === 0) {
- {{ emptyTableExplanation() }} + {{ "noApplicationsMatchTheseFilters" | i18n }}
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts index b4cbbc5c436..b79f5160bf7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.spec.ts @@ -1,8 +1,9 @@ +import { Signal, WritableSignal } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { DrawerDetails, @@ -11,6 +12,7 @@ import { ReportStatus, RiskInsightsDataService, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; +import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers"; import { RiskInsightsEnrichedData } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-data-service.types"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,9 +25,18 @@ import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks import { ApplicationsComponent } from "./applications.component"; +// Mock ResizeObserver +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + // Helper type to access protected members in tests type ComponentWithProtectedMembers = ApplicationsComponent & { dataSource: TableDataSource; + selectedUrls: WritableSignal>; + filteredTableData: Signal; }; describe("ApplicationsComponent", () => { @@ -83,7 +94,10 @@ describe("ApplicationsComponent", () => { { provide: RiskInsightsDataService, useValue: mockDataService }, { provide: ActivatedRoute, - useValue: { snapshot: { paramMap: { get: (): string | null => null } } }, + useValue: { + paramMap: of(convertToParamMap({})), + snapshot: { paramMap: convertToParamMap({}) }, + }, }, { provide: AccessIntelligenceSecurityTasksService, useValue: mockSecurityTasksService }, ], @@ -91,6 +105,7 @@ describe("ApplicationsComponent", () => { fixture = TestBed.createComponent(ApplicationsComponent); component = fixture.componentInstance; + fixture.detectChanges(); }); afterEach(() => { @@ -247,4 +262,185 @@ describe("ApplicationsComponent", () => { expect(capturedBlobData).not.toContain("Slack"); }); }); + + describe("checkbox selection", () => { + const mockApplicationData: ApplicationTableDataSource[] = [ + { + applicationName: "GitHub", + passwordCount: 10, + atRiskPasswordCount: 3, + memberCount: 5, + atRiskMemberCount: 2, + isMarkedAsCritical: true, + atRiskCipherIds: ["cipher1" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher1" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Slack", + passwordCount: 8, + atRiskPasswordCount: 1, + memberCount: 4, + atRiskMemberCount: 1, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher2" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher2" as CipherId], + iconCipher: undefined, + }, + { + applicationName: "Jira", + passwordCount: 12, + atRiskPasswordCount: 4, + memberCount: 6, + atRiskMemberCount: 3, + isMarkedAsCritical: false, + atRiskCipherIds: ["cipher3" as CipherId], + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher3" as CipherId], + iconCipher: undefined, + }, + ]; + + beforeEach(() => { + // Emit mock data through the data service observable to populate the table + enrichedReportData$.next({ + reportData: mockApplicationData, + summaryData: createNewSummaryData(), + applicationData: [], + creationDate: new Date(), + }); + }); + + describe("onCheckboxChange", () => { + it("should add application to selectedUrls when checked is true", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should remove application from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: false }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(false); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.size).toBe(1); + }); + + it("should handle multiple applications being selected", () => { + // act + component.onCheckboxChange({ applicationName: "GitHub", checked: true }); + component.onCheckboxChange({ applicationName: "Slack", checked: true }); + component.onCheckboxChange({ applicationName: "Jira", checked: true }); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + + describe("onSelectAllChange", () => { + it("should add all visible applications to selectedUrls when checked is true", () => { + // act + component.onSelectAllChange(true); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + + it("should remove all applications from selectedUrls when checked is false", () => { + // arrange + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub", "Slack"])); + + // act + component.onSelectAllChange(false); + + // assert + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.size).toBe(0); + }); + + it("should only add visible filtered applications when filter is applied", () => { + // arrange - apply filter to only show critical apps + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => app.isMarkedAsCritical; + fixture.detectChanges(); + + // act + component.onSelectAllChange(true); + + // assert - only GitHub is critical + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should only remove visible filtered applications when unchecking with filter applied", () => { + // arrange - select all apps first, then apply filter to only show non-critical apps + (component as ComponentWithProtectedMembers).selectedUrls.set( + new Set(["GitHub", "Slack", "Jira"]), + ); + + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - uncheck with filter applied + component.onSelectAllChange(false); + + // assert - GitHub (critical) should still be selected + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(false); + expect(selectedUrls.has("Jira")).toBe(false); + expect(selectedUrls.size).toBe(1); + }); + + it("should preserve existing selections when checking select all with filter", () => { + // arrange - select a non-visible app + (component as ComponentWithProtectedMembers).selectedUrls.set(new Set(["GitHub"])); + + // apply filter to hide GitHub + (component as ComponentWithProtectedMembers).dataSource.filter = ( + app: ApplicationTableDataSource, + ) => !app.isMarkedAsCritical; + fixture.detectChanges(); + + // act - select all visible (non-critical apps) + component.onSelectAllChange(true); + + // assert - GitHub should still be selected + visible apps + const selectedUrls = (component as ComponentWithProtectedMembers).selectedUrls(); + expect(selectedUrls.has("GitHub")).toBe(true); + expect(selectedUrls.has("Slack")).toBe(true); + expect(selectedUrls.has("Jira")).toBe(true); + expect(selectedUrls.size).toBe(3); + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts index 0020106ba7d..659e099641c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/applications.component.ts @@ -89,6 +89,9 @@ export class ApplicationsComponent implements OnInit { // Standard properties protected readonly dataSource = new TableDataSource(); protected readonly searchControl = new FormControl("", { nonNullable: true }); + protected readonly filteredTableData = toSignal(this.dataSource.connect(), { + initialValue: [], + }); // Template driven properties protected readonly selectedUrls = signal(new Set()); @@ -117,15 +120,36 @@ export class ApplicationsComponent implements OnInit { icon: " ", }, ]); - protected readonly emptyTableExplanation = signal(""); + + // Computed property that returns only selected applications that are currently visible in filtered data + readonly visibleSelectedApps = computed(() => { + const filteredData = this.filteredTableData(); + const selected = this.selectedUrls(); + + if (!filteredData || selected.size === 0) { + return new Set(); + } + + const visibleSelected = new Set(); + filteredData.forEach((row) => { + if (selected.has(row.applicationName)) { + visibleSelected.add(row.applicationName); + } + }); + + return visibleSelected; + }); readonly allSelectedAppsAreCritical = computed(() => { - if (!this.dataSource.filteredData || this.selectedUrls().size == 0) { + const visibleSelected = this.visibleSelectedApps(); + const filteredData = this.filteredTableData(); + + if (!filteredData || visibleSelected.size === 0) { return false; } - return this.dataSource.filteredData - .filter((row) => this.selectedUrls().has(row.applicationName)) + return filteredData + .filter((row) => visibleSelected.has(row.applicationName)) .every((row) => row.isMarkedAsCritical); }); @@ -174,6 +198,9 @@ export class ApplicationsComponent implements OnInit { })); this.dataSource.data = tableDataWithIcon; this.totalApplicationsCount.set(report.reportData.length); + this.criticalApplicationsCount.set( + report.reportData.filter((app) => app.isMarkedAsCritical).length, + ); } else { this.dataSource.data = []; } @@ -183,16 +210,6 @@ export class ApplicationsComponent implements OnInit { }, }); - this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ - next: (criticalReport) => { - if (criticalReport != null) { - this.criticalApplicationsCount.set(criticalReport.reportData.length); - } else { - this.criticalApplicationsCount.set(0); - } - }, - }); - combineLatest([ this.searchControl.valueChanges.pipe(startWith("")), this.selectedFilterObservable, @@ -210,21 +227,6 @@ export class ApplicationsComponent implements OnInit { this.dataSource.filter = (app) => filterFunction(app) && app.applicationName.toLowerCase().includes(searchText.toLowerCase()); - - // filter selectedUrls down to only applications showing with active filters - const filteredUrls = new Set(); - this.dataSource.filteredData?.forEach((row) => { - if (this.selectedUrls().has(row.applicationName)) { - filteredUrls.add(row.applicationName); - } - }); - this.selectedUrls.set(filteredUrls); - - if (this.dataSource?.filteredData?.length === 0) { - this.emptyTableExplanation.set(this.i18nService.t("noApplicationsMatchTheseFilters")); - } else { - this.emptyTableExplanation.set(""); - } }); } @@ -232,15 +234,16 @@ export class ApplicationsComponent implements OnInit { this.selectedFilter.set(value); } - markAppsAsCritical = async () => { + async markAppsAsCritical() { this.updatingCriticalApps.set(true); - const count = this.selectedUrls().size; + const visibleSelected = this.visibleSelectedApps(); + const count = visibleSelected.size; this.dataService - .saveCriticalApplications(Array.from(this.selectedUrls())) + .saveCriticalApplications(Array.from(visibleSelected)) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ variant: "success", title: "", @@ -248,6 +251,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ @@ -257,17 +263,17 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } - unmarkAppsAsCritical = async () => { + async unmarkAppsAsCritical() { this.updatingCriticalApps.set(true); - const appsToUnmark = this.selectedUrls(); + const appsToUnmark = this.visibleSelectedApps(); this.dataService .removeCriticalApplications(appsToUnmark) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: () => { + next: (response) => { this.toastService.showToast({ message: this.i18nService.t( "numApplicationsUnmarkedCriticalSuccess", @@ -277,6 +283,9 @@ export class ApplicationsComponent implements OnInit { }); this.selectedUrls.set(new Set()); this.updatingCriticalApps.set(false); + this.criticalApplicationsCount.set( + response?.data?.summaryData?.totalCriticalApplicationCount ?? 0, + ); }, error: () => { this.toastService.showToast({ @@ -286,7 +295,7 @@ export class ApplicationsComponent implements OnInit { }); }, }); - }; + } async requestPasswordChange() { const orgId = this.organizationId(); @@ -318,24 +327,38 @@ export class ApplicationsComponent implements OnInit { } } - showAppAtRiskMembers = async (applicationName: string) => { + async showAppAtRiskMembers(applicationName: string) { await this.dataService.setDrawerForAppAtRiskMembers(applicationName); - }; + } - onCheckboxChange = (applicationName: string, event: Event) => { - const isChecked = (event.target as HTMLInputElement).checked; + onCheckboxChange({ applicationName, checked }: { applicationName: string; checked: boolean }) { this.selectedUrls.update((selectedUrls) => { const nextSelected = new Set(selectedUrls); - if (isChecked) { + if (checked) { nextSelected.add(applicationName); } else { nextSelected.delete(applicationName); } return nextSelected; }); - }; + } - downloadApplicationsCSV = () => { + onSelectAllChange(checked: boolean) { + const filteredData = this.filteredTableData(); + if (!filteredData) { + return; + } + + this.selectedUrls.update((selectedUrls) => { + const nextSelected = new Set(selectedUrls); + filteredData.forEach((row) => + checked ? nextSelected.add(row.applicationName) : nextSelected.delete(row.applicationName), + ); + return nextSelected; + }); + } + + downloadApplicationsCSV() { try { const data = this.dataSource.filteredData; if (!data || data.length === 0) { @@ -368,5 +391,5 @@ export class ApplicationsComponent implements OnInit { } catch (error) { this.logService.error("Failed to download applications CSV", error); } - }; + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html index 05dec048328..ddbc977fc13 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.html @@ -33,7 +33,7 @@ bitCheckbox type="checkbox" [checked]="selectedUrls().has(row.applicationName)" - (change)="checkboxChange()(row.applicationName, $event)" + (change)="checkboxChanged($event.target, row.applicationName)" /> { selectAllCheckboxEl = fixture.debugElement.query(By.css('[data-testid="selectAll"]')); }); - it("should check all rows in table when checked", () => { + it("should emit selectAllChange event with true when checked", () => { // arrange const selectedUrls = new Set(); const dataSource = new TableDataSource(); @@ -121,18 +121,19 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.has("google.com")).toBe(true); - expect(selectedUrls.has("facebook.com")).toBe(true); - expect(selectedUrls.has("twitter.com")).toBe(true); - expect(selectedUrls.size).toBe(3); + expect(selectAllChangeSpy).toHaveBeenCalledWith(true); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); - it("should uncheck all rows in table when unchecked", () => { + it("should emit selectAllChange event with false when unchecked", () => { // arrange const selectedUrls = new Set(["google.com", "facebook.com", "twitter.com"]); const dataSource = new TableDataSource(); @@ -142,12 +143,16 @@ describe("AppTableRowScrollableM11Component", () => { fixture.componentRef.setInput("dataSource", dataSource); fixture.detectChanges(); + const selectAllChangeSpy = jest.fn(); + fixture.componentInstance.selectAllChange.subscribe(selectAllChangeSpy); + // act selectAllCheckboxEl.nativeElement.click(); fixture.detectChanges(); // assert - expect(selectedUrls.size).toBe(0); + expect(selectAllChangeSpy).toHaveBeenCalledWith(false); + expect(selectAllChangeSpy).toHaveBeenCalledTimes(1); }); it("should become checked when all rows in table are checked", () => { @@ -178,4 +183,59 @@ describe("AppTableRowScrollableM11Component", () => { expect(selectAllCheckboxEl.nativeElement.checked).toBe(false); }); }); + + describe("individual row checkbox", () => { + it("should emit checkboxChange event with correct parameters when checkboxChanged is called", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: true, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with checked=false when checkbox is unchecked", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: false } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "google.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "google.com", + checked: false, + }); + expect(checkboxChangeSpy).toHaveBeenCalledTimes(1); + }); + + it("should emit checkboxChange with correct applicationName for different applications", () => { + // arrange + const checkboxChangeSpy = jest.fn(); + fixture.componentInstance.checkboxChange.subscribe(checkboxChangeSpy); + + const mockTarget = { checked: true } as HTMLInputElement; + + // act + fixture.componentInstance.checkboxChanged(mockTarget, "facebook.com"); + + // assert + expect(checkboxChangeSpy).toHaveBeenCalledWith({ + applicationName: "facebook.com", + checked: true, + }); + }); + }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts index a23d1855ba5..65cfb8d092e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable-m11.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { MenuModule, TableDataSource, TableModule, TooltipDirective } from "@bitwarden/components"; @@ -30,7 +30,8 @@ export class AppTableRowScrollableM11Component { readonly selectedUrls = input>(); readonly openApplication = input(""); readonly showAppAtRiskMembers = input<(applicationName: string) => void>(); - readonly checkboxChange = input<(applicationName: string, $event: Event) => void>(); + readonly checkboxChange = output<{ applicationName: string; checked: boolean }>(); + readonly selectAllChange = output(); allAppsSelected(): boolean { const tableData = this.dataSource()?.filteredData; @@ -43,20 +44,13 @@ export class AppTableRowScrollableM11Component { return tableData.length > 0 && tableData.every((row) => selectedUrls.has(row.applicationName)); } + checkboxChanged(target: HTMLInputElement, applicationName: string) { + const checked = target.checked; + this.checkboxChange.emit({ applicationName, checked }); + } + selectAllChanged(target: HTMLInputElement) { const checked = target.checked; - - const tableData = this.dataSource()?.filteredData; - const selectedUrls = this.selectedUrls(); - - if (!tableData || !selectedUrls) { - return false; - } - - if (checked) { - tableData.forEach((row) => selectedUrls.add(row.applicationName)); - } else { - selectedUrls.clear(); - } + this.selectAllChange.emit(checked); } } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html index 0b5a63c8f03..c816861b623 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.html @@ -10,14 +10,9 @@ >
- -
- - {{ stepConfig[progressStep()].message | i18n }} - - - {{ "thisMightTakeFewMinutes" | i18n }} - -
+ + + {{ stepConfig[progressStep()].message | i18n }} +
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts index 45b28dae470..9df729b9645 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/report-loading.component.ts @@ -6,12 +6,12 @@ import { ProgressModule } from "@bitwarden/components"; // Map of progress step to display config const ProgressStepConfig = Object.freeze({ - [ReportProgress.FetchingMembers]: { message: "fetchingMemberData", progress: 20 }, - [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswordHealth", progress: 40 }, - [ReportProgress.CalculatingRisks]: { message: "calculatingRiskScores", progress: 60 }, - [ReportProgress.GeneratingReport]: { message: "generatingReportData", progress: 80 }, - [ReportProgress.Saving]: { message: "savingReport", progress: 95 }, - [ReportProgress.Complete]: { message: "compilingInsights", progress: 100 }, + [ReportProgress.FetchingMembers]: { message: "reviewingMemberData", progress: 20 }, + [ReportProgress.AnalyzingPasswords]: { message: "analyzingPasswords", progress: 40 }, + [ReportProgress.CalculatingRisks]: { message: "calculatingRisks", progress: 60 }, + [ReportProgress.GeneratingReport]: { message: "generatingReports", progress: 80 }, + [ReportProgress.Saving]: { message: "compilingInsightsProgress", progress: 95 }, + [ReportProgress.Complete]: { message: "reportGenerationDone", progress: 100 }, } as const); // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts index 18e6dc7e362..4a1718d4a19 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts @@ -1,11 +1,11 @@ import { Component } from "@angular/core"; +import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; -import { FilterIntegrationsPipe } from "../integrations.pipe"; -import { OrganizationIntegrationsState } from "../organization-integrations.state"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -17,7 +17,7 @@ import { OrganizationIntegrationsState } from "../organization-integrations.stat export class DeviceManagementComponent { integrations = this.state.integrations; - constructor(private state: OrganizationIntegrationsState) {} + constructor(private state: IntegrationStateService) {} get IntegrationType(): typeof IntegrationType { return IntegrationType; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts index 70b17cabd35..0e8296d5a29 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts @@ -1,11 +1,11 @@ import { Component } from "@angular/core"; +import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; -import { FilterIntegrationsPipe } from "../integrations.pipe"; -import { OrganizationIntegrationsState } from "../organization-integrations.state"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -16,7 +16,8 @@ import { OrganizationIntegrationsState } from "../organization-integrations.stat }) export class EventManagementComponent { integrations = this.state.integrations; - constructor(private state: OrganizationIntegrationsState) {} + + constructor(private state: IntegrationStateService) {} get IntegrationType(): typeof IntegrationType { return IntegrationType; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html index 792606cbfe0..19c2b6728e2 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html @@ -10,26 +10,26 @@
- @if (linkURL) { + @if (linkURL()) { }

- {{ name }} + {{ name() }} @if (showConnectedBadge()) { @if (isConnected) { @@ -41,15 +41,15 @@ }

- @if (description) { -

{{ description }}

+ @if (description()) { +

{{ description() }}

} - @if (canSetupConnection) { + @if (canSetupConnection()) { } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts index 928bb9488b3..6f5c67a2cab 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts @@ -8,6 +8,7 @@ import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-i import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; @@ -35,6 +36,7 @@ describe("IntegrationCardComponent", () => { const mockIntegrationService = mock(); const dialogService = mock(); const toastService = mock(); + const stateService = mock(); const systemTheme$ = new BehaviorSubject(ThemeType.Light); const usersPreferenceTheme$ = new BehaviorSubject(ThemeType.Light); @@ -59,6 +61,7 @@ describe("IntegrationCardComponent", () => { { provide: OrganizationIntegrationService, useValue: mockIntegrationService }, { provide: ToastService, useValue: toastService }, { provide: DialogService, useValue: dialogService }, + { provide: IntegrationStateService, useValue: stateService }, ], }).compileComponents(); }); @@ -67,9 +70,9 @@ describe("IntegrationCardComponent", () => { fixture = TestBed.createComponent(IntegrationCardComponent); component = fixture.componentInstance; - component.name = "Integration Name"; - component.image = "test-image.png"; - component.linkURL = "https://example.com/"; + fixture.componentRef.setInput("name", "Integration Name"); + fixture.componentRef.setInput("image", "test-image.png"); + fixture.componentRef.setInput("linkURL", "https://example.com/"); mockI18nService.t.mockImplementation((key) => key); fixture.detectChanges(); @@ -88,7 +91,7 @@ describe("IntegrationCardComponent", () => { }); it("assigns external rel attribute", () => { - component.externalURL = true; + fixture.componentRef.setInput("externalURL", true); fixture.detectChanges(); const link = fixture.nativeElement.querySelector("a"); @@ -107,26 +110,27 @@ describe("IntegrationCardComponent", () => { }); it("shows when expiration is in the future", () => { - component.newBadgeExpiration = "2023-09-02"; + fixture.componentRef.setInput("newBadgeExpiration", "2023-09-02"); expect(component.showNewBadge()).toBe(true); }); it("does not show when expiration is not set", () => { + fixture.componentRef.setInput("newBadgeExpiration", undefined); expect(component.showNewBadge()).toBe(false); }); it("does not show when expiration is in the past", () => { - component.newBadgeExpiration = "2023-08-31"; + fixture.componentRef.setInput("newBadgeExpiration", "2023-08-31"); expect(component.showNewBadge()).toBe(false); }); it("does not show when expiration is today", () => { - component.newBadgeExpiration = "2023-09-01"; + fixture.componentRef.setInput("newBadgeExpiration", "2023-09-01"); expect(component.showNewBadge()).toBe(false); }); it("does not show when expiration is invalid", () => { - component.newBadgeExpiration = "not-a-date"; + fixture.componentRef.setInput("newBadgeExpiration", "not-a-date"); expect(component.showNewBadge()).toBe(false); }); }); @@ -138,12 +142,12 @@ describe("IntegrationCardComponent", () => { fixture.detectChanges(); - expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + expect(component.imageEle().nativeElement.src).toContain("test-image.png"); }); describe("user prefers the system theme", () => { beforeEach(() => { - component.imageDarkMode = "test-image-dark.png"; + fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png"); }); it("sets image src to imageDarkMode", () => { @@ -152,24 +156,24 @@ describe("IntegrationCardComponent", () => { fixture.detectChanges(); - expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + expect(component.imageEle().nativeElement.src).toContain("test-image-dark.png"); }); it("sets image src to light mode image", () => { - component.imageEle.nativeElement.src = "test-image-dark.png"; + component.imageEle().nativeElement.src = "test-image-dark.png"; usersPreferenceTheme$.next(ThemeType.System); systemTheme$.next(ThemeType.Light); fixture.detectChanges(); - expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + expect(component.imageEle().nativeElement.src).toContain("test-image.png"); }); }); describe("user prefers dark mode", () => { beforeEach(() => { - component.imageDarkMode = "test-image-dark.png"; + fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png"); }); it("updates image to dark mode", () => { @@ -178,24 +182,24 @@ describe("IntegrationCardComponent", () => { fixture.detectChanges(); - expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + expect(component.imageEle().nativeElement.src).toContain("test-image-dark.png"); }); }); describe("user prefers light mode", () => { beforeEach(() => { - component.imageDarkMode = "test-image-dark.png"; + fixture.componentRef.setInput("imageDarkMode", "test-image-dark.png"); }); it("updates image to light mode", () => { - component.imageEle.nativeElement.src = "test-image-dark.png"; + component.imageEle().nativeElement.src = "test-image-dark.png"; systemTheme$.next(ThemeType.Dark); // system theme shouldn't matter usersPreferenceTheme$.next(ThemeType.Light); fixture.detectChanges(); - expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + expect(component.imageEle().nativeElement.src).toContain("test-image.png"); }); }); }); @@ -211,57 +215,52 @@ describe("IntegrationCardComponent", () => { }); it("returns false when newBadgeExpiration is undefined", () => { - component.newBadgeExpiration = undefined; + fixture.componentRef.setInput("newBadgeExpiration", undefined); expect(component.showNewBadge()).toBe(false); }); it("returns false when newBadgeExpiration is an invalid date", () => { - component.newBadgeExpiration = "invalid-date"; + fixture.componentRef.setInput("newBadgeExpiration", "invalid-date"); expect(component.showNewBadge()).toBe(false); }); it("returns true when newBadgeExpiration is in the future", () => { - component.newBadgeExpiration = "2024-06-02"; + fixture.componentRef.setInput("newBadgeExpiration", "2024-06-02"); expect(component.showNewBadge()).toBe(true); }); it("returns false when newBadgeExpiration is today", () => { - component.newBadgeExpiration = "2024-06-01"; + fixture.componentRef.setInput("newBadgeExpiration", "2024-06-01"); expect(component.showNewBadge()).toBe(false); }); it("returns false when newBadgeExpiration is in the past", () => { - component.newBadgeExpiration = "2024-05-31"; + fixture.componentRef.setInput("newBadgeExpiration", "2024-05-31"); expect(component.showNewBadge()).toBe(false); }); }); describe("showConnectedBadge", () => { it("returns true when canSetupConnection is true", () => { - component.canSetupConnection = true; + fixture.componentRef.setInput("canSetupConnection", true); expect(component.showConnectedBadge()).toBe(true); }); it("returns false when canSetupConnection is false", () => { - component.canSetupConnection = false; - expect(component.showConnectedBadge()).toBe(false); - }); - - it("returns false when canSetupConnection is undefined", () => { - component.canSetupConnection = undefined; + fixture.componentRef.setInput("canSetupConnection", false); expect(component.showConnectedBadge()).toBe(false); }); }); describe("setupConnection", () => { beforeEach(() => { - component.integrationSettings = { + fixture.componentRef.setInput("integrationSettings", { organizationIntegration: { id: "integration-id", configuration: {}, integrationConfiguration: [{ id: "config-id" }], }, name: OrganizationIntegrationServiceName.CrowdStrike, - } as any; + } as any); component.organizationId = "org-id" as any; jest.resetAllMocks(); }); @@ -311,10 +310,10 @@ describe("IntegrationCardComponent", () => { }); it("should call saveHec if isUpdateAvailable is false", async () => { - component.integrationSettings = { + fixture.componentRef.setInput("integrationSettings", { organizationIntegration: null, name: OrganizationIntegrationServiceName.CrowdStrike, - } as any; + } as any); component.organizationId = "org-id" as any; (openHecConnectDialog as jest.Mock).mockReturnValue({ @@ -376,10 +375,10 @@ describe("IntegrationCardComponent", () => { }); it("should not call delete if no existing configuration", async () => { - component.integrationSettings = { + fixture.componentRef.setInput("integrationSettings", { organizationIntegration: null, name: OrganizationIntegrationServiceName.CrowdStrike, - } as any; + } as any); component.organizationId = "org-id" as any; (openHecConnectDialog as jest.Mock).mockReturnValue({ diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index f423a9b86d9..809c69cf635 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -3,9 +3,9 @@ import { Component, ElementRef, Inject, - Input, + input, OnDestroy, - ViewChild, + viewChild, } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs"; @@ -20,7 +20,11 @@ import { } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder"; import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; -import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { + IntegrationModificationResult, + OrganizationIntegrationService, +} from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; @@ -52,30 +56,13 @@ import { }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("imageEle") imageEle!: ElementRef; - - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() name: string = ""; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() image: string = ""; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() imageDarkMode: string = ""; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() linkURL: string = ""; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() integrationSettings!: Integration; - - /** Adds relevant `rel` attribute to external links */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() externalURL?: boolean; + readonly imageEle = viewChild.required>("imageEle"); + readonly name = input.required(); + readonly image = input.required(); + readonly imageDarkMode = input.required(); + readonly linkURL = input.required(); + readonly integrationSettings = input.required(); + readonly externalURL = input.required(); /** * Date of when the new badge should be hidden. @@ -83,15 +70,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { * * @example "2024-12-31" */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() newBadgeExpiration?: string; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() description?: string; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() canSetupConnection?: boolean; + readonly newBadgeExpiration = input(undefined); + readonly description = input(""); + readonly canSetupConnection = input(false); organizationId: OrganizationId; @@ -104,6 +85,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private organizationIntegrationService: OrganizationIntegrationService, private toastService: ToastService, private i18nService: I18nService, + protected state: IntegrationStateService, ) { this.organizationId = this.activatedRoute.snapshot.paramMap.get( "organizationId", @@ -115,7 +97,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { .pipe(takeUntil(this.destroyed$)) .subscribe(([theme, systemTheme]) => { // When the card doesn't have a dark mode image, exit early - if (!this.imageDarkMode) { + if (!this.imageDarkMode()) { return; } @@ -124,13 +106,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { // use the system theme to determine the image const prefersDarkMode = systemTheme === ThemeType.Dark; - this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image; + this.imageEle().nativeElement.src = prefersDarkMode ? this.imageDarkMode() : this.image(); } else if (theme === ThemeType.Dark) { // When the user's preference is dark mode, use the dark mode image - this.imageEle.nativeElement.src = this.imageDarkMode; + this.imageEle().nativeElement.src = this.imageDarkMode(); } else { // Otherwise use the light mode image - this.imageEle.nativeElement.src = this.image; + this.imageEle().nativeElement.src = this.image(); } }); } @@ -142,11 +124,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { /** Show the "new" badge when expiration is in the future */ showNewBadge() { - if (!this.newBadgeExpiration) { + if (!this.newBadgeExpiration()) { return false; } - const expirationDate = new Date(this.newBadgeExpiration); + const expirationDate = new Date(this.newBadgeExpiration() ?? "undefined"); // Do not show the new badge for invalid dates if (isNaN(expirationDate.getTime())) { @@ -157,26 +139,26 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } get isConnected(): boolean { - return !!this.integrationSettings.organizationIntegration?.configuration; + return !!this.integrationSettings().organizationIntegration?.configuration; } showConnectedBadge(): boolean { - return this.canSetupConnection ?? false; + return this.canSetupConnection(); } get isUpdateAvailable(): boolean { - return !!this.integrationSettings.organizationIntegration; + return !!this.integrationSettings().organizationIntegration; } async setupConnection() { - if (this.integrationSettings?.integrationType === null) { + if (this.integrationSettings()?.integrationType === null) { return; } - if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) { + if (this.integrationSettings()?.integrationType === OrganizationIntegrationType.Datadog) { const dialog = openDatadogConnectDialog(this.dialogService, { data: { - settings: this.integrationSettings, + settings: this.integrationSettings(), }, }); @@ -187,11 +169,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { () => this.deleteDatadog(), (res) => this.saveDatadog(res), ); - } else if (this.integrationSettings.name === OrganizationIntegrationServiceName.Huntress) { + } else if (this.integrationSettings().name === OrganizationIntegrationServiceName.Huntress) { // Huntress uses HEC protocol but has its own dialog const dialog = openHuntressConnectDialog(this.dialogService, { data: { - settings: this.integrationSettings, + settings: this.integrationSettings(), }, }); @@ -206,7 +188,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { // invoke the dialog to connect the integration const dialog = openHecConnectDialog(this.dialogService, { data: { - settings: this.integrationSettings, + settings: this.integrationSettings(), }, }); @@ -228,13 +210,17 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { config: OrgIntegrationConfiguration, template: OrgIntegrationTemplate, ): Promise { - let response = { mustBeOwner: false, success: false }; + let response: IntegrationModificationResult = { + mustBeOwner: false, + success: false, + organizationIntegrationResult: undefined, + }; if (this.isUpdateAvailable) { // retrieve org integration and configuration ids - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationId = this.integrationSettings().organizationIntegration?.id; const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + this.integrationSettings().organizationIntegration?.integrationConfiguration[0]?.id; if (!orgIntegrationId || !orgIntegrationConfigurationId) { throw Error("Organization Integration ID or Configuration ID is missing"); @@ -264,10 +250,21 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { return; } + // update local state with the new integration settings + if (response.success && response.organizationIntegrationResult) { + this.state.updateIntegrationSettings( + this.integrationSettings().name, + response.organizationIntegrationResult, + ); + } + this.toastService.showToast({ variant: "success", title: "", - message: this.i18nService.t("success"), + message: this.i18nService.t( + "integrationConnectedSuccessfully", + this.integrationSettings().name, + ), }); } @@ -275,9 +272,9 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { * Generic delete method */ private async deleteIntegration(): Promise { - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationId = this.integrationSettings().organizationIntegration?.id; const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + this.integrationSettings().organizationIntegration?.integrationConfiguration[0]?.id; if (!orgIntegrationId || !orgIntegrationConfigurationId) { throw Error("Organization Integration ID or Configuration ID is missing"); @@ -294,6 +291,10 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { return; } + if (response.success) { + this.state.deleteIntegrationSettings(this.integrationSettings().name); + } + this.toastService.showToast({ variant: "success", title: "", @@ -347,11 +348,11 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { const config = OrgIntegrationBuilder.buildHecConfiguration( result.url, result.bearerToken, - this.integrationSettings.name as OrganizationIntegrationServiceName, + this.integrationSettings().name as OrganizationIntegrationServiceName, ); const template = OrgIntegrationBuilder.buildHecTemplate( result.index, - this.integrationSettings.name as OrganizationIntegrationServiceName, + this.integrationSettings().name as OrganizationIntegrationServiceName, ); await this.saveIntegration(OrganizationIntegrationType.Hec, config, template); @@ -385,7 +386,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { async saveDatadog(result: DatadogConnectDialogResult) { const config = OrgIntegrationBuilder.buildDataDogConfiguration(result.url, result.apiKey); const template = OrgIntegrationBuilder.buildDataDogTemplate( - this.integrationSettings.name as OrganizationIntegrationServiceName, + this.integrationSettings().name as OrganizationIntegrationServiceName, ); await this.saveIntegration(OrganizationIntegrationType.Datadog, config, template); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.html index 8127c6a0343..2fdabf73490 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.html @@ -1,10 +1,10 @@
    - @for (integration of integrations; track integration) { + @for (integration of integrations(); track integration) {
  • { provide: ToastService, useValue: mock(), }, + { provide: IntegrationStateService, useValue: mock() }, ], }); fixture = TestBed.createComponent(IntegrationGridComponent); component = fixture.componentInstance; - component.integrations = integrations; - component.ariaI18nKey = "integrationCardAriaLabel"; - component.tooltipI18nKey = "integrationCardTooltip"; + fixture.componentRef.setInput("integrations", integrations); + fixture.componentRef.setInput("ariaI18nKey", "integrationCardAriaLabel"); + fixture.componentRef.setInput("tooltipI18nKey", "integrationCardTooltip"); fixture.detectChanges(); }); it("lists all integrations", () => { - expect(component.integrations).toEqual(integrations); + expect(component.integrations()).toEqual(integrations); const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); @@ -94,20 +96,20 @@ describe("IntegrationGridComponent", () => { }); it("assigns the correct attributes to IntegrationCardComponent", () => { - expect(component.integrations).toEqual(integrations); + expect(component.integrations()).toEqual(integrations); const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent))[1]; - expect(card.componentInstance.name).toBe("SDK 2"); - expect(card.componentInstance.image).toBe("test-image2.png"); - expect(card.componentInstance.linkURL).toBe("https://example.com/2"); + expect(card.componentInstance.name()).toBe("SDK 2"); + expect(card.componentInstance.image()).toBe("test-image2.png"); + expect(card.componentInstance.linkURL()).toBe("https://example.com/2"); }); it("assigns `externalURL` for SDKs", () => { const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); - expect(card[0].componentInstance.externalURL).toBe(false); - expect(card[1].componentInstance.externalURL).toBe(true); + expect(card[0].componentInstance.externalURL()).toBe(false); + expect(card[1].componentInstance.externalURL()).toBe(true); }); it("has a tool tip and aria label attributes", () => { diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts index 19f15d1caea..ff029eb0b36 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from "@angular/core"; +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; import { IntegrationType } from "@bitwarden/common/enums"; @@ -6,24 +6,16 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationCardComponent } from "../integration-card/integration-card.component"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "app-integration-grid", templateUrl: "./integration-grid.component.html", imports: [IntegrationCardComponent, SharedModule], }) export class IntegrationGridComponent { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() integrations: Integration[] = []; - - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() ariaI18nKey: string = "integrationCardAriaLabel"; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() tooltipI18nKey: string = "integrationCardTooltip"; + readonly integrations = input.required(); + readonly ariaI18nKey = input("integrationCardAriaLabel"); + readonly tooltipI18nKey = input("integrationCardTooltip"); protected IntegrationType = IntegrationType; } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index 786aa70bfc5..60a9f42e8c3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -1,11 +1,10 @@ import { Component } from "@angular/core"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { OrganizationIntegrationsState } from "./organization-integrations.state"; - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -16,7 +15,7 @@ import { OrganizationIntegrationsState } from "./organization-integrations.state export class AdminConsoleIntegrationsComponent { organization = this.state.organization; - constructor(private state: OrganizationIntegrationsState) {} + constructor(private state: IntegrationStateService) {} // use in the view get IntegrationType(): typeof IntegrationType { diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts index 626fc5dee88..fed98345fc7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts @@ -7,7 +7,6 @@ import { DeviceManagementComponent } from "./device-management/device-management import { EventManagementComponent } from "./event-management/event-management.component"; import { AdminConsoleIntegrationsComponent } from "./integrations.component"; import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver"; -import { OrganizationIntegrationsState } from "./organization-integrations.state"; import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component"; import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component"; @@ -19,7 +18,7 @@ const routes: Routes = [ titleId: "integrations", }, component: AdminConsoleIntegrationsComponent, - providers: [OrganizationIntegrationsState, OrganizationIntegrationsResolver], + providers: [OrganizationIntegrationsResolver], resolve: { integrations: OrganizationIntegrationsResolver }, children: [ { path: "", redirectTo: "single-sign-on", pathMatch: "full" }, diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts index 33f389a92a9..e24bb33238d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts @@ -4,6 +4,7 @@ import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-manage import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service"; import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service"; import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { safeProvider } from "@bitwarden/ui-common"; @@ -11,6 +12,7 @@ import { EventManagementComponent } from "./event-management/event-management.co import { AdminConsoleIntegrationsComponent } from "./integrations.component"; import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module"; import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver"; +import { OrganizationIntegrationsState } from "./organization-integrations.state"; import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component"; import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component"; @@ -40,6 +42,11 @@ import { UserProvisioningComponent } from "./user-provisioning/user-provisioning useClass: OrganizationIntegrationConfigurationApiService, deps: [ApiService], }), + safeProvider({ + provide: IntegrationStateService, + useClass: OrganizationIntegrationsState, + useAngularDecorators: true, + }), ], }) export class OrganizationIntegrationsModule {} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts index 39bd0cc1dcc..16e5113f4d7 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts @@ -7,6 +7,7 @@ import { Integration } from "@bitwarden/bit-common/dirt/organization-integration import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -15,8 +16,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { getById } from "@bitwarden/common/platform/misc"; -import { OrganizationIntegrationsState } from "./organization-integrations.state"; - @Injectable() export class OrganizationIntegrationsResolver implements Resolve { constructor( @@ -24,7 +23,7 @@ export class OrganizationIntegrationsResolver implements Resolve { private accountService: AccountService, private configService: ConfigService, private organizationIntegrationService: OrganizationIntegrationService, - private state: OrganizationIntegrationsState, + private state: IntegrationStateService, ) {} async resolve(route: ActivatedRouteSnapshot): Promise { @@ -262,6 +261,7 @@ export class OrganizationIntegrationsResolver implements Resolve { name: OrganizationIntegrationServiceName.Huntress, linkURL: "https://bitwarden.com/help/huntress-siem/", image: "../../../../../../../images/integrations/logo-huntress-siem.svg", + imageDarkMode: "../../../../../../../images/integrations/logo-huntress-siem-darkmode.svg", type: IntegrationType.EVENT, description: "huntressEventIntegrationDesc", canSetupConnection: true, diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts index 5e7e6a78ba4..5a059986f47 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts @@ -1,10 +1,12 @@ import { Injectable, signal } from "@angular/core"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { OrganizationIntegration } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @Injectable() -export class OrganizationIntegrationsState { +export class OrganizationIntegrationsState implements IntegrationStateService { private readonly _integrations = signal([]); private readonly _organization = signal(undefined); @@ -12,11 +14,38 @@ export class OrganizationIntegrationsState { integrations = this._integrations.asReadonly(); organization = this._organization.asReadonly(); - setOrganization(val: Organization | null) { + setOrganization(val: Organization | undefined) { this._organization.set(val ?? undefined); } setIntegrations(val: Integration[]) { this._integrations.set(val); } + + updateIntegrationSettings( + integrationName: string, + updatedIntegrationSettings: OrganizationIntegration, + ) { + const integrations = this._integrations(); + const index = integrations.findIndex((i) => i.name === integrationName); + if (index >= 0) { + const updatedIntegrations = integrations.map((integration, i) => + i === index + ? { ...integration, organizationIntegration: updatedIntegrationSettings } + : integration, + ); + this.setIntegrations(updatedIntegrations); + } + } + + deleteIntegrationSettings(integrationName: string) { + const integrations = this._integrations(); + const index = integrations.findIndex((i) => i.name === integrationName); + if (index >= 0) { + const updatedIntegrations = integrations.map((integration, i) => + i === index ? { ...integration, organizationIntegration: undefined } : integration, + ); + this.setIntegrations(updatedIntegrations); + } + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts index d0d2a1666f2..f8c529a1456 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts @@ -1,11 +1,11 @@ import { Component } from "@angular/core"; +import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; -import { FilterIntegrationsPipe } from "../integrations.pipe"; -import { OrganizationIntegrationsState } from "../organization-integrations.state"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -18,5 +18,5 @@ export class SingleSignOnComponent { integrations = this.state.integrations; IntegrationType = IntegrationType; - constructor(private state: OrganizationIntegrationsState) {} + constructor(private state: IntegrationStateService) {} } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts index f484674d224..8e1b544b75e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts @@ -1,11 +1,11 @@ import { Component } from "@angular/core"; +import { FilterIntegrationsPipe } from "@bitwarden/bit-common/dirt/organization-integrations/shared/filter-integrations.pipe"; +import { IntegrationStateService } from "@bitwarden/bit-common/dirt/organization-integrations/shared/integration-state.service"; import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationGridComponent } from "../integration-grid/integration-grid.component"; -import { FilterIntegrationsPipe } from "../integrations.pipe"; -import { OrganizationIntegrationsState } from "../organization-integrations.state"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -18,7 +18,7 @@ export class UserProvisioningComponent { organization = this.state.organization; integrations = this.state.integrations; - constructor(private state: OrganizationIntegrationsState) {} + constructor(private state: IntegrationStateService) {} get IntegrationType(): typeof IntegrationType { return IntegrationType; diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html index d12341deff1..ca2863f8235 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html @@ -9,7 +9,7 @@ > } @@ -22,11 +22,11 @@ @if (isLoading) {
    - +

    {{ "loading" | i18n }}

    } @else { @@ -42,7 +42,12 @@
    - +
    - - - {{ "loading" | i18n }} - - -

    {{ "noEventsInList" | i18n }}

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

    {{ "noEventsInList" | i18n }}

    + } + @if (events() && events().length) { + + + + {{ "timestamp" | i18n }} + {{ "client" | i18n }} + {{ "event" | i18n }} + + + + @for (e of events(); track i; let i = $index) { + + {{ e.date | date: "medium" }} + + {{ e.appName }} + + + + } + + + } + @if (continuationToken) { + + } +
    +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 5968933064d..525d658f233 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { takeUntil } from "rxjs"; @@ -17,9 +17,8 @@ import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export" import { ServiceAccountEventLogApiService } from "./service-account-event-log-api.service"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "sm-service-accounts-events", templateUrl: "./service-accounts-events.component.html", standalone: false, @@ -69,7 +68,7 @@ export class ServiceAccountEventsComponent async load() { await this.refreshEvents(); - this.loaded = true; + this.loaded.set(true); } protected requestEvents(startDate: string, endDate: string, continuationToken: string) { diff --git a/eslint.config.mjs b/eslint.config.mjs index 974aaafeef6..2e35b011c73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -208,6 +208,7 @@ export default tseslint.config( { ignoreIfHas: ["bitPasswordInputToggle"] }, ], "@bitwarden/components/no-bwi-class-usage": "warn", + "@bitwarden/components/no-icon-children-in-bit-button": "warn", }, }, diff --git a/jest.config.js b/jest.config.js index bfe447f7a53..b0e4a9f6b13 100644 --- a/jest.config.js +++ b/jest.config.js @@ -61,6 +61,8 @@ module.exports = { "/libs/vault/jest.config.js", "/libs/auto-confirm/jest.config.js", "/libs/subscription/jest.config.js", + "/libs/unlock/jest.config.js", + "/libs/user-crypto-management/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/angular/src/auth/device-management/device-management-table.component.html b/libs/angular/src/auth/device-management/device-management-table.component.html index 72187b2a2fc..4c7e0bcb92d 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.html +++ b/libs/angular/src/auth/device-management/device-management-table.component.html @@ -18,7 +18,7 @@
    - +
    diff --git a/libs/angular/src/auth/device-management/device-management-table.component.ts b/libs/angular/src/auth/device-management/device-management-table.component.ts index 36edf6dd336..3e3555cee13 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.ts +++ b/libs/angular/src/auth/device-management/device-management-table.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { BadgeModule, ButtonModule, + IconModule, LinkModule, TableDataSource, TableModule, @@ -21,7 +22,15 @@ import { DeviceDisplayData } from "./device-management.component"; standalone: true, selector: "auth-device-management-table", templateUrl: "./device-management-table.component.html", - imports: [BadgeModule, ButtonModule, CommonModule, JslibModule, LinkModule, TableModule], + imports: [ + BadgeModule, + ButtonModule, + CommonModule, + IconModule, + JslibModule, + LinkModule, + TableModule, + ], }) export class DeviceManagementTableComponent implements OnChanges { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals diff --git a/libs/angular/src/auth/device-management/device-management.component.html b/libs/angular/src/auth/device-management/device-management.component.html index 2a91c2daae2..1b113082254 100644 --- a/libs/angular/src/auth/device-management/device-management.component.html +++ b/libs/angular/src/auth/device-management/device-management.component.html @@ -8,7 +8,7 @@ [bitPopoverTriggerFor]="infoPopover" position="right-start" > - + @@ -23,7 +23,11 @@ @if (initializing) {
    - +
    } @else { diff --git a/libs/angular/src/auth/device-management/device-management.component.ts b/libs/angular/src/auth/device-management/device-management.component.ts index d8f8cc10df4..c697ea44099 100644 --- a/libs/angular/src/auth/device-management/device-management.component.ts +++ b/libs/angular/src/auth/device-management/device-management.component.ts @@ -19,7 +19,7 @@ import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; -import { ButtonModule, DialogService, PopoverModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, IconModule, PopoverModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { LoginApprovalDialogComponent } from "../login-approval"; @@ -62,6 +62,7 @@ export interface DeviceDisplayData { DeviceManagementItemGroupComponent, DeviceManagementTableComponent, I18nPipe, + IconModule, PopoverModule, ], }) diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.html b/libs/angular/src/auth/environment-selector/environment-selector.component.html index 72d7355c399..a1115d94712 100644 --- a/libs/angular/src/auth/environment-selector/environment-selector.component.html +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.html @@ -12,12 +12,12 @@ [attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'" (click)="toggle(region.key)" > - + > {{ region.domain }} @@ -41,7 +41,7 @@ {{ data.selectedRegion?.domain || ("selfHostedServer" | i18n) }} - +
    diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.ts b/libs/angular/src/auth/environment-selector/environment-selector.component.ts index 89366f47b70..79df6a2d992 100644 --- a/libs/angular/src/auth/environment-selector/environment-selector.component.ts +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.ts @@ -13,6 +13,7 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, + IconModule, LinkModule, MenuModule, ToastService, @@ -26,7 +27,7 @@ import { I18nPipe } from "@bitwarden/ui-common"; selector: "environment-selector", templateUrl: "environment-selector.component.html", standalone: true, - imports: [CommonModule, I18nPipe, MenuModule, LinkModule, TypographyModule], + imports: [CommonModule, I18nPipe, IconModule, LinkModule, MenuModule, TypographyModule], }) export class EnvironmentSelectorComponent implements OnDestroy { protected ServerEnvironmentType = Region; diff --git a/libs/angular/src/auth/login-approval/login-approval-dialog.component.html b/libs/angular/src/auth/login-approval/login-approval-dialog.component.html index f2850406235..fdaf6584251 100644 --- a/libs/angular/src/auth/login-approval/login-approval-dialog.component.html +++ b/libs/angular/src/auth/login-approval/login-approval-dialog.component.html @@ -4,7 +4,11 @@
    - +
    diff --git a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts index 54906047535..36e553aa7d9 100644 --- a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts +++ b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts @@ -20,6 +20,7 @@ import { ButtonModule, DialogModule, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; @@ -35,7 +36,7 @@ export interface LoginApprovalDialogParams { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "login-approval-dialog.component.html", - imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule], + imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, IconModule, JslibModule], }) export class LoginApprovalDialogComponent implements OnInit, OnDestroy { authRequestId: string; diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.html b/libs/angular/src/auth/password-management/change-password/change-password.component.html index 7604ffacea7..c147af329ce 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.html +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.html @@ -1,10 +1,9 @@ @if (initializing) { - - {{ "loading" | i18n }} + } @else { { + const ctx = + "Could not set initial password for TDE user with Manage Account Recovery permission."; + + assertTruthy(credentials.newPassword, "newPassword", ctx); + assertTruthy(credentials.salt, "salt", ctx); + assertNonNullish(credentials.kdfConfig, "kdfConfig", ctx); + assertNonNullish(credentials.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertTruthy(credentials.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(credentials.orgId, "orgId", ctx); + assertNonNullish(credentials.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish + assertTruthy(userId, "userId", ctx); + + const { + newPassword, + salt, + kdfConfig, + newPasswordHint, + orgSsoIdentifier, + orgId, + resetPasswordAutoEnroll, + } = credentials; + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (!userKey) { + throw new Error("userKey not found."); + } + + const authenticationData: MasterPasswordAuthenticationData = + await this.masterPasswordService.makeMasterPasswordAuthenticationData( + newPassword, + kdfConfig, + salt, + ); + + const unlockData: MasterPasswordUnlockData = + await this.masterPasswordService.makeMasterPasswordUnlockData( + newPassword, + kdfConfig, + salt, + userKey, + ); + + const request = SetPasswordRequest.newConstructor( + authenticationData, + unlockData, + newPasswordHint, + orgSsoIdentifier, + null, // no KeysRequest for TDE user because they already have a key pair + ); + + await this.masterPasswordApiService.setPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + // User now has a password so update decryption state + await this.masterPasswordService.setMasterPasswordUnlockData(unlockData, userId); + await this.updateLegacyState( + newPassword, + unlockData.kdf, + new EncString(unlockData.masterKeyWrappedUserKey), + userId, + unlockData, + ); + + if (resetPasswordAutoEnroll) { + await this.handleResetPasswordAutoEnroll( + authenticationData.masterPasswordAuthenticationHash, + orgId, + userId, + userKey, + ); + } + } + /** * @deprecated To be removed in PM-28143 */ @@ -441,7 +577,19 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi await this.masterPasswordService.setMasterPasswordUnlockData(masterPasswordUnlockData, userId); } - private async handleResetPasswordAutoEnroll( + /** + * @deprecated To be removed in PM-28143 + * + * This method is now deprecated because it is used with the deprecated `setInitialPassword()` method, + * which handles both JIT MP and TDE + Permission user flows. + * + * Since these methods can handle the JIT MP flow - which creates a new user key and sets it to state - we + * must retreive that user key here in this method. + * + * But the new handleResetPasswordAutoEnroll() method is only used in the TDE + Permission user case, in which + * case we already have the user key and can simply pass it through via method parameter ( @see handleResetPasswordAutoEnroll ) + */ + private async handleResetPasswordAutoEnrollOld( masterKeyHash: string, orgId: string, userId: UserId, @@ -483,4 +631,43 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi enrollmentRequest, ); } + + private async handleResetPasswordAutoEnroll( + masterKeyHash: string, + orgId: string, + userId: UserId, + userKey: UserKey, + ) { + const organizationKeys = await this.organizationApiService.getKeys(orgId); + + if (organizationKeys == null) { + throw new Error( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + } + + const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey); + + // RSA encrypt user key with organization public key + const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + orgPublicKey, + ); + + if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) { + throw new Error( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + } + + const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = masterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + + await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( + orgId, + userId, + enrollmentRequest, + ); + } } diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index 8b64e20ce7b..5543a631225 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -31,6 +31,9 @@ import { } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { + MasterKeyWrappedUserKey, + MasterPasswordAuthenticationData, + MasterPasswordAuthenticationHash, MasterPasswordSalt, MasterPasswordUnlockData, } from "@bitwarden/common/key-management/master-password/types/master-password.types"; @@ -62,6 +65,8 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordTdeOffboardingCredentialsOld, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -237,7 +242,7 @@ describe("DefaultSetInitialPasswordService", () => { } } - // Mock handleResetPasswordAutoEnroll() values + // Mock handleResetPasswordAutoEnrollOld() values if (config.resetPasswordAutoEnroll) { organizationApiService.getKeys.mockResolvedValue(organizationKeys); encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); @@ -753,10 +758,160 @@ describe("DefaultSetInitialPasswordService", () => { }); }); - describe("setInitialPasswordTdeOffboarding(...)", () => { - // Mock function parameters + describe("setInitialPasswordTdeOffboarding()", () => { + // Mock method parameters let credentials: SetInitialPasswordTdeOffboardingCredentials; + // Mock method data + let userKey: UserKey; + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + let request: UpdateTdeOffboardingPasswordRequest; + + beforeEach(() => { + credentials = { + newPassword: "new-Password", + salt: "salt" as MasterPasswordSalt, + kdfConfig: DEFAULT_KDF_CONFIG, + newPasswordHint: "newPasswordHint", + }; + + userKey = makeSymmetricCryptoKey(64) as UserKey; + + authenticationData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + + unlockData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + + request = UpdateTdeOffboardingPasswordRequest.newConstructorWithHint( + authenticationData, + unlockData, + credentials.newPasswordHint, + ); + + keyService.userKey$.mockReturnValue(of(userKey)); + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); + }); + + describe("general error handling", () => { + ["newPassword", "salt"].forEach((key) => { + it(`should throw if ${key} is an empty string (falsy) on the SetInitialPasswordTdeOffboardingCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeOffboardingCredentials = { + ...credentials, + [key]: "", + }; + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow(`${key} is falsy. Could not set initial password.`); + }); + }); + + ["kdfConfig", "newPasswordHint"].forEach((key) => { + it(`should throw if ${key} is null/undefined on the SetInitialPasswordTdeOffboardingCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeOffboardingCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + `${key} is null or undefined. Could not set initial password.`, + ); + }); + }); + + it(`should throw if the userId was not passed in`, async () => { + // Arrange + userId = null; + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userId not found. Could not set password."); + }); + + it(`should throw if the userKey was not found`, async () => { + // Arrange + keyService.userKey$.mockReturnValue(of(null)); + + // Act + const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userKey not found. Could not set password."); + }); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + ); + + expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + userKey, + ); + }); + + it("should call the API method to set a master password", async () => { + // Act + await sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); + expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledWith( + request, + ); + }); + + it("should set the ForceSetPasswordReason to None", async () => { + // Act + await sut.setInitialPasswordTdeOffboarding(credentials, userId); + + // Assert + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + }); + + /** + * @deprecated To be removed in PM-28143. When you remove this, check also if there are any imports/properties + * in the test setup above that are now un-used and can also be removed. + */ + describe("setInitialPasswordTdeOffboardingOld(...)", () => { + // Mock function parameters + let credentials: SetInitialPasswordTdeOffboardingCredentialsOld; + beforeEach(() => { // Mock function parameters credentials = { @@ -781,7 +936,7 @@ describe("DefaultSetInitialPasswordService", () => { request.masterPasswordHint = credentials.newPasswordHint; // Act - await sut.setInitialPasswordTdeOffboarding(credentials, userId); + await sut.setInitialPasswordTdeOffboardingOld(credentials, userId); // Assert expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); @@ -796,7 +951,7 @@ describe("DefaultSetInitialPasswordService", () => { setupTdeOffboardingMocks(); // Act - await sut.setInitialPasswordTdeOffboarding(credentials, userId); + await sut.setInitialPasswordTdeOffboardingOld(credentials, userId); // Assert expect(masterPasswordApiService.putUpdateTdeOffboardingPassword).toHaveBeenCalledTimes(1); @@ -811,13 +966,13 @@ describe("DefaultSetInitialPasswordService", () => { ["newMasterKey", "newServerMasterKeyHash", "newPasswordHint"].forEach((key) => { it(`should throw if ${key} is not provided on the SetInitialPasswordTdeOffboardingCredentials object`, async () => { // Arrange - const invalidCredentials: SetInitialPasswordTdeOffboardingCredentials = { + const invalidCredentials: SetInitialPasswordTdeOffboardingCredentialsOld = { ...credentials, [key]: null, }; // Act - const promise = sut.setInitialPasswordTdeOffboarding(invalidCredentials, userId); + const promise = sut.setInitialPasswordTdeOffboardingOld(invalidCredentials, userId); // Assert await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`); @@ -829,7 +984,7 @@ describe("DefaultSetInitialPasswordService", () => { userId = null; // Act - const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); // Assert await expect(promise).rejects.toThrow("userId not found. Could not set password."); @@ -840,7 +995,7 @@ describe("DefaultSetInitialPasswordService", () => { keyService.userKey$.mockReturnValue(of(null)); // Act - const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); // Assert await expect(promise).rejects.toThrow("userKey not found. Could not set password."); @@ -853,7 +1008,7 @@ describe("DefaultSetInitialPasswordService", () => { setupTdeOffboardingMocks(); // Act - const promise = sut.setInitialPasswordTdeOffboarding(credentials, userId); + const promise = sut.setInitialPasswordTdeOffboardingOld(credentials, userId); // Assert await expect(promise).rejects.toThrow( @@ -1104,4 +1259,285 @@ describe("DefaultSetInitialPasswordService", () => { await expect(promise).rejects.toThrow("Unexpected V2 account cryptographic state"); }); }); + + describe("setInitialPasswordTdeUserWithPermission()", () => { + // Mock method parameters + let credentials: SetInitialPasswordTdeUserWithPermissionCredentials; + + // Mock method data + let authenticationData: MasterPasswordAuthenticationData; + let unlockData: MasterPasswordUnlockData; + let setPasswordRequest: SetPasswordRequest; + let userDecryptionOptions: UserDecryptionOptions; + + beforeEach(() => { + // Mock method parameters + credentials = { + newPassword: "newPassword123!", + salt: "user@example.com" as MasterPasswordSalt, + kdfConfig: DEFAULT_KDF_CONFIG, + newPasswordHint: "newPasswordHint", + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId" as OrganizationId, + resetPasswordAutoEnroll: false, + }; + + // Mock method data + userKey = makeSymmetricCryptoKey(64) as UserKey; + keyService.userKey$.mockReturnValue(of(userKey)); + + authenticationData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterPasswordAuthenticationHash: + "masterPasswordAuthenticationHash" as MasterPasswordAuthenticationHash, + }; + masterPasswordService.makeMasterPasswordAuthenticationData.mockResolvedValue( + authenticationData, + ); + + unlockData = { + salt: credentials.salt, + kdf: credentials.kdfConfig, + masterKeyWrappedUserKey: "masterKeyWrappedUserKey" as MasterKeyWrappedUserKey, + } as MasterPasswordUnlockData; + masterPasswordService.makeMasterPasswordUnlockData.mockResolvedValue(unlockData); + + setPasswordRequest = SetPasswordRequest.newConstructor( + authenticationData, + unlockData, + credentials.newPasswordHint, + credentials.orgSsoIdentifier, + null, // no KeysRequest for TDE user because they already have a key pair + ); + + userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: false }); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + }); + + describe("general error handling", () => { + ["newPassword", "salt", "orgSsoIdentifier", "orgId"].forEach((key) => { + it(`should throw if ${key} is an empty string (falsy) on the SetInitialPasswordTdeUserWithPermissionCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + ...credentials, + [key]: "", + }; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + `${key} is falsy. Could not set initial password for TDE user with Manage Account Recovery permission.`, + ); + }); + }); + + ["kdfConfig", "newPasswordHint", "resetPasswordAutoEnroll"].forEach((key) => { + it(`should throw if ${key} is null on the SetInitialPasswordTdeUserWithPermissionCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(invalidCredentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + `${key} is null or undefined. Could not set initial password for TDE user with Manage Account Recovery permission.`, + ); + }); + }); + + it("should throw if userId is not given", async () => { + // Arrange + userId = null; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "userId is falsy. Could not set initial password for TDE user with Manage Account Recovery permission.", + ); + }); + }); + + it("should throw if the userKey is not found", async () => { + // Arrange + keyService.userKey$.mockReturnValue(of(null)); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow("userKey not found."); + }); + + it("should call makeMasterPasswordAuthenticationData and makeMasterPasswordUnlockData with the correct parameters", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.makeMasterPasswordAuthenticationData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + ); + + expect(masterPasswordService.makeMasterPasswordUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + credentials.kdfConfig, + credentials.salt, + userKey, + ); + }); + + it("should call the API method to set a master password", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledTimes(1); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should set MasterPasswordUnlockData to state", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(masterPasswordService.setMasterPasswordUnlockData).toHaveBeenCalledWith( + unlockData, + userId, + ); + }); + + it("should update legacy state", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, + expect.objectContaining({ hasMasterPassword: true }), + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + new EncString(unlockData.masterKeyWrappedUserKey), + userId, + ); + expect(masterPasswordService.setLegacyMasterKeyFromUnlockData).toHaveBeenCalledWith( + credentials.newPassword, + unlockData, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it("should NOT handle reset password (account recovery) auto enroll", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + let organizationKeys: OrganizationKeysResponse; + let orgPublicKeyEncryptedUserKey: EncString; + let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest; + + beforeEach(() => { + credentials.resetPasswordAutoEnroll = true; + + organizationKeys = { + privateKey: "orgPrivateKey", + publicKey: "orgPublicKey", + } as OrganizationKeysResponse; + organizationApiService.getKeys.mockResolvedValue(organizationKeys); + + orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey"); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); + + enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = + authenticationData.masterPasswordAuthenticationHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + }); + + it("should throw if organization keys are not found", async () => { + // Arrange + organizationApiService.getKeys.mockResolvedValue(null); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + }); + + it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => { + // Arrange + encryptService.encapsulateKeyUnsigned.mockResolvedValue(null); + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + }); + + it("should throw if orgPublicKeyEncryptedUserKey.encryptedString is not found", async () => { + // Arrange + orgPublicKeyEncryptedUserKey.encryptedString = null; + + // Act + const promise = sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + }); + + it("should call the API method to handle reset password (account recovery) auto enroll", async () => { + // Act + await sut.setInitialPasswordTdeUserWithPermission(credentials, userId); + + // Assert + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledTimes(1); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + }); + }); + }); }); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html index 8033d2022f4..18c8c833066 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html @@ -1,10 +1,6 @@ @if (initializing) {
    - +
    } @else { @if (userType === SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER_UNTRUSTED_DEVICE) { diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index 7850a980eef..7a2279f305a 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -37,6 +37,7 @@ import { ButtonModule, CalloutComponent, DialogService, + IconModule, ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -47,6 +48,8 @@ import { SetInitialPasswordCredentials, SetInitialPasswordService, SetInitialPasswordTdeOffboardingCredentials, + SetInitialPasswordTdeOffboardingCredentialsOld, + SetInitialPasswordTdeUserWithPermissionCredentials, SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; @@ -55,7 +58,14 @@ import { @Component({ standalone: true, templateUrl: "set-initial-password.component.html", - imports: [ButtonModule, CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe], + imports: [ + ButtonModule, + CalloutComponent, + CommonModule, + IconModule, + InputPasswordComponent, + I18nPipe, + ], }) export class SetInitialPasswordComponent implements OnInit { protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; @@ -183,10 +193,22 @@ export class SetInitialPasswordComponent implements OnInit { break; } case SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { + await this.setInitialPasswordTdeUserWithPermission(passwordInputResult); + return; // EARLY RETURN for flagged logic + } + await this.setInitialPassword(passwordInputResult); + break; case SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER: - await this.setInitialPasswordTdeOffboarding(passwordInputResult); + if (passwordInputResult.newApisWithInputPasswordFlagEnabled) { + await this.setInitialPasswordTdeOffboarding(passwordInputResult); + return; + } + + await this.setInitialPasswordTdeOffboardingOld(passwordInputResult); + break; default: this.logService.error( @@ -382,7 +404,85 @@ export class SetInitialPasswordComponent implements OnInit { } } + private async setInitialPasswordTdeUserWithPermission(passwordInputResult: PasswordInputResult) { + const ctx = + "Could not set initial password for TDE user with Manage Account Recovery permission."; + + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertTruthy(this.orgSsoIdentifier, "orgSsoIdentifier", ctx); + assertTruthy(this.orgId, "orgId", ctx); + assertNonNullish(this.resetPasswordAutoEnroll, "resetPasswordAutoEnroll", ctx); // can have `false` as a valid value, so check non-nullish + assertTruthy(this.userId, "userId", ctx); + + try { + const credentials: SetInitialPasswordTdeUserWithPermissionCredentials = { + newPassword: passwordInputResult.newPassword, + salt: passwordInputResult.salt, + kdfConfig: passwordInputResult.kdfConfig, + newPasswordHint: passwordInputResult.newPasswordHint, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId as OrganizationId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + }; + + await this.setInitialPasswordService.setInitialPasswordTdeUserWithPermission( + credentials, + this.userId, + ); + + this.showSuccessToastByUserType(); + + this.submitting = false; + await this.router.navigate(["vault"]); + } catch (e) { + this.logService.error("Error setting initial password", e); + this.validationService.showError(e); + this.submitting = false; + } + } + private async setInitialPasswordTdeOffboarding(passwordInputResult: PasswordInputResult) { + const ctx = "Could not set initial password."; + assertTruthy(passwordInputResult.newPassword, "newPassword", ctx); + assertTruthy(passwordInputResult.salt, "salt", ctx); + assertNonNullish(passwordInputResult.kdfConfig, "kdfConfig", ctx); + assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish + assertTruthy(this.userId, "userId", ctx); + + try { + const credentials: SetInitialPasswordTdeOffboardingCredentials = { + newPassword: passwordInputResult.newPassword, + salt: passwordInputResult.salt, + kdfConfig: passwordInputResult.kdfConfig, + newPasswordHint: passwordInputResult.newPasswordHint, + }; + + await this.setInitialPasswordService.setInitialPasswordTdeOffboarding( + credentials, + this.userId, + ); + + this.showSuccessToastByUserType(); + + // TODO: investigate refactoring logout and follow-up routing in https://bitwarden.atlassian.net/browse/PM-32660 + await this.logoutService.logout(this.userId); + // navigate to root so redirect guard can properly route next active user or null user to correct page + await this.router.navigate(["/"]); + } catch (e) { + this.logService.error("Error setting initial password during TDE offboarding", e); + this.validationService.showError(e); + } finally { + this.submitting = false; + } + } + + /** + * @deprecated To be removed in PM-28143 + */ + private async setInitialPasswordTdeOffboardingOld(passwordInputResult: PasswordInputResult) { const ctx = "Could not set initial password."; assertTruthy(passwordInputResult.newMasterKey, "newMasterKey", ctx); assertTruthy(passwordInputResult.newServerMasterKeyHash, "newServerMasterKeyHash", ctx); @@ -390,13 +490,13 @@ export class SetInitialPasswordComponent implements OnInit { assertNonNullish(passwordInputResult.newPasswordHint, "newPasswordHint", ctx); // can have an empty string as a valid value, so check non-nullish try { - const credentials: SetInitialPasswordTdeOffboardingCredentials = { + const credentials: SetInitialPasswordTdeOffboardingCredentialsOld = { newMasterKey: passwordInputResult.newMasterKey, newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, newPasswordHint: passwordInputResult.newPasswordHint, }; - await this.setInitialPasswordService.setInitialPasswordTdeOffboarding( + await this.setInitialPasswordService.setInitialPasswordTdeOffboardingOld( credentials, this.userId, ); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts index 70318be3393..8b1de87043d 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -55,12 +55,32 @@ export interface SetInitialPasswordCredentials { salt: MasterPasswordSalt; } -export interface SetInitialPasswordTdeOffboardingCredentials { +export interface SetInitialPasswordTdeUserWithPermissionCredentials { + newPassword: string; + salt: MasterPasswordSalt; + kdfConfig: KdfConfig; + newPasswordHint: string; + orgSsoIdentifier: string; + orgId: OrganizationId; + resetPasswordAutoEnroll: boolean; +} + +/** + * @deprecated To be removed in PM-28143 + */ +export interface SetInitialPasswordTdeOffboardingCredentialsOld { newMasterKey: MasterKey; newServerMasterKeyHash: string; newPasswordHint: string; } +export interface SetInitialPasswordTdeOffboardingCredentials { + newPassword: string; + salt: MasterPasswordSalt; + kdfConfig: KdfConfig; + newPasswordHint: string; +} + /** * Credentials required to initialize a just-in-time (JIT) provisioned user with a master password. */ @@ -104,6 +124,21 @@ export abstract class SetInitialPasswordService { ) => Promise; /** + * Sets an initial password for an existing authed TDE user who has been given the + * Manage Account Recovery permission: + * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} + * + * @param credentials An object of the credentials needed to set the initial password + * @throws If any property on the `credentials` object not found, or if userKey is not found + */ + abstract setInitialPasswordTdeUserWithPermission: ( + credentials: SetInitialPasswordTdeUserWithPermissionCredentials, + userId: UserId, + ) => Promise; + + /** + * @deprecated To be removed in PM-28143 + * * Sets an initial password for a user who logs in after their org offboarded from * trusted device encryption and is now a master-password-encryption org: * - {@link SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER} @@ -111,8 +146,8 @@ export abstract class SetInitialPasswordService { * @param passwordInputResult credentials object received from the `InputPasswordComponent` * @param userId the account `userId` */ - abstract setInitialPasswordTdeOffboarding: ( - credentials: SetInitialPasswordTdeOffboardingCredentials, + abstract setInitialPasswordTdeOffboardingOld: ( + credentials: SetInitialPasswordTdeOffboardingCredentialsOld, userId: UserId, ) => Promise; @@ -125,4 +160,18 @@ export abstract class SetInitialPasswordService { credentials: InitializeJitPasswordCredentials, userId: UserId, ): Promise; + + /** + * Sets an initial password for a user who logs in after their org offboarded from + * trusted device encryption and is now a master-password-encryption org: + * - {@link SetInitialPasswordUserType.OFFBOARDED_TDE_ORG_USER} + * + * @param credentials An object of the credentials needed to set the initial password + * @param userId the account `userId` + * @throws if `userId`, `userKey`, or necessary credentials are not found + */ + abstract setInitialPasswordTdeOffboarding: ( + credentials: SetInitialPasswordTdeOffboardingCredentials, + userId: UserId, + ) => Promise; } diff --git a/libs/angular/src/directives/a11y-invalid.directive.ts b/libs/angular/src/directives/a11y-invalid.directive.ts deleted file mode 100644 index 032c08d5332..00000000000 --- a/libs/angular/src/directives/a11y-invalid.directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, ElementRef, OnDestroy, OnInit } from "@angular/core"; -import { NgControl } from "@angular/forms"; -import { Subscription } from "rxjs"; - -@Directive({ - selector: "[appA11yInvalid]", - standalone: false, -}) -export class A11yInvalidDirective implements OnDestroy, OnInit { - private sub: Subscription; - - constructor( - private el: ElementRef, - private formControlDirective: NgControl, - ) {} - - ngOnInit() { - this.sub = this.formControlDirective.control.statusChanges.subscribe((status) => { - if (status === "INVALID") { - this.el.nativeElement.setAttribute("aria-invalid", "true"); - } else if (status === "VALID") { - this.el.nativeElement.setAttribute("aria-invalid", "false"); - } - }); - } - - ngOnDestroy() { - this.sub?.unsubscribe(); - } -} diff --git a/libs/angular/src/directives/copy-text.directive.ts b/libs/angular/src/directives/copy-text.directive.ts deleted file mode 100644 index aefb26ef07e..00000000000 --- a/libs/angular/src/directives/copy-text.directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -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({ - selector: "[appCopyText]", - standalone: false, -}) -export class CopyTextDirective { - constructor( - private el: ElementRef, - private platformUtilsService: PlatformUtilsService, - ) {} - - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input("appCopyText") copyText: string; - - @HostListener("copy") onCopy() { - if (window == null) { - return; - } - - const timeout = this.platformUtilsService.getClientType() === ClientType.Desktop ? 100 : 0; - setTimeout(() => { - this.platformUtilsService.copyToClipboard(this.copyText, { window: window }); - }, timeout); - } -} diff --git a/libs/angular/src/directives/fallback-src.directive.ts b/libs/angular/src/directives/fallback-src.directive.ts deleted file mode 100644 index b63dc8671cf..00000000000 --- a/libs/angular/src/directives/fallback-src.directive.ts +++ /dev/null @@ -1,25 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, ElementRef, HostListener, Input } from "@angular/core"; - -@Directive({ - selector: "[appFallbackSrc]", - standalone: false, -}) -export class FallbackSrcDirective { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input("appFallbackSrc") appFallbackSrc: string; - - /** Only try setting the fallback once. This prevents an infinite loop if the fallback itself is missing. */ - private tryFallback = true; - - constructor(private el: ElementRef) {} - - @HostListener("error") onError() { - if (this.tryFallback) { - this.el.nativeElement.src = this.appFallbackSrc; - this.tryFallback = false; - } - } -} diff --git a/libs/angular/src/directives/true-false-value.directive.ts b/libs/angular/src/directives/true-false-value.directive.ts deleted file mode 100644 index 78c1b4647c6..00000000000 --- a/libs/angular/src/directives/true-false-value.directive.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Directive, ElementRef, forwardRef, HostListener, Input, Renderer2 } from "@angular/core"; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; - -// ref: https://juristr.com/blog/2018/02/ng-true-value-directive/ -@Directive({ - selector: "input[type=checkbox][appTrueFalseValue]", - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TrueFalseValueDirective), - multi: true, - }, - ], - standalone: false, -}) -export class TrueFalseValueDirective implements ControlValueAccessor { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() trueValue: boolean | string = true; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() falseValue: boolean | string = false; - - constructor( - private elementRef: ElementRef, - private renderer: Renderer2, - ) {} - - @HostListener("change", ["$event"]) - onHostChange(ev: any) { - this.propagateChange(ev.target.checked ? this.trueValue : this.falseValue); - } - - writeValue(obj: any): void { - if (obj === this.trueValue) { - this.renderer.setProperty(this.elementRef.nativeElement, "checked", true); - } else { - this.renderer.setProperty(this.elementRef.nativeElement, "checked", false); - } - } - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(fn: any): void { - /* nothing */ - } - - setDisabledState?(isDisabled: boolean): void { - /* nothing */ - } - - private propagateChange = (_: any) => { - /* nothing */ - }; -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index c3670148d67..663e2f39b52 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -26,11 +26,8 @@ import { import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; import { NotPremiumDirective } from "./billing/directives/not-premium.directive"; -import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; import { BoxRowDirective } from "./directives/box-row.directive"; -import { CopyTextDirective } from "./directives/copy-text.directive"; -import { FallbackSrcDirective } from "./directives/fallback-src.directive"; import { IfFeatureDirective } from "./directives/if-feature.directive"; import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive"; import { InputVerbatimDirective } from "./directives/input-verbatim.directive"; @@ -38,18 +35,23 @@ import { LaunchClickDirective } from "./directives/launch-click.directive"; import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TextDragDirective } from "./directives/text-drag.directive"; -import { TrueFalseValueDirective } from "./directives/true-false-value.directive"; import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe"; import { PluralizePipe } from "./pipes/pluralize.pipe"; -import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe"; import { SearchPipe } from "./pipes/search.pipe"; import { UserNamePipe } from "./pipes/user-name.pipe"; import { UserTypePipe } from "./pipes/user-type.pipe"; import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe"; -import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe"; import { I18nPipe } from "./platform/pipes/i18n.pipe"; import { IconComponent } from "./vault/components/icon.component"; +/** + * @deprecated In 95% of cases you want I18nPipe from `@bitwarden/ui-common`. In the other 5% + * directly import the relevant directive/pipe/component. If you need one of the non standalone + * pipes/directives/components, make it standalone and import directly. + * + * This module is overly large and adds many unrelated modules to your dependency tree. + * https://angular.dev/guide/ngmodules/overview recommends not using `NgModule`s for new code. + */ @NgModule({ imports: [ ToastModule.forRoot({ @@ -82,57 +84,45 @@ import { IconComponent } from "./vault/components/icon.component"; AutofocusDirective, ], declarations: [ - A11yInvalidDirective, ApiActionDirective, BoxRowDirective, - CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, - FallbackSrcDirective, I18nPipe, IconComponent, InputStripSpacesDirective, InputVerbatimDirective, NotPremiumDirective, - SearchCiphersPipe, SearchPipe, StopClickDirective, StopPropDirective, - TrueFalseValueDirective, LaunchClickDirective, UserNamePipe, UserTypePipe, IfFeatureDirective, - FingerprintPipe, TwoFactorIconComponent, ], exports: [ - A11yInvalidDirective, A11yTitleDirective, ApiActionDirective, AutofocusDirective, ToastModule, BoxRowDirective, - CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, - FallbackSrcDirective, I18nPipe, IconComponent, InputStripSpacesDirective, InputVerbatimDirective, NotPremiumDirective, - SearchCiphersPipe, SearchPipe, StopClickDirective, StopPropDirective, - TrueFalseValueDirective, CopyClickDirective, LaunchClickDirective, UserNamePipe, UserTypePipe, IfFeatureDirective, - FingerprintPipe, TwoFactorIconComponent, TextDragDirective, ], @@ -143,7 +133,6 @@ import { IconComponent } from "./vault/components/icon.component"; SearchPipe, UserNamePipe, UserTypePipe, - FingerprintPipe, PluralizePipe, ], }) diff --git a/libs/angular/src/key-management/encrypted-migration/encrypted-migrations-scheduler.service.ts b/libs/angular/src/key-management/encrypted-migration/encrypted-migrations-scheduler.service.ts index dd248a582d3..19186e8a668 100644 --- a/libs/angular/src/key-management/encrypted-migration/encrypted-migrations-scheduler.service.ts +++ b/libs/angular/src/key-management/encrypted-migration/encrypted-migrations-scheduler.service.ts @@ -38,7 +38,7 @@ export const ENCRYPTED_MIGRATION_DISMISSED = new UserKeyDefinition( }, ); const DISMISS_TIME_HOURS = 24; -const VAULT_ROUTES = ["/vault", "/tabs/vault", "/tabs/current"]; +const VAULT_ROUTES = ["/new-vault", "/vault", "/tabs/vault", "/tabs/current"]; /** * This services schedules encrypted migrations for users on clients that are interactive (non-cli), and handles manual interaction, diff --git a/libs/angular/src/pipes/search-ciphers.pipe.ts b/libs/angular/src/pipes/search-ciphers.pipe.ts deleted file mode 100644 index cbb595280af..00000000000 --- a/libs/angular/src/pipes/search-ciphers.pipe.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Pipe, PipeTransform } from "@angular/core"; - -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -@Pipe({ - name: "searchCiphers", - standalone: false, -}) -export class SearchCiphersPipe implements PipeTransform { - transform(ciphers: CipherView[], searchText: string, deleted = false): CipherView[] { - if (ciphers == null || ciphers.length === 0) { - return []; - } - - if (searchText == null || searchText.length < 2) { - return ciphers.filter((c) => { - return deleted !== c.isDeleted; - }); - } - - searchText = searchText.trim().toLowerCase(); - return ciphers.filter((c) => { - if (deleted !== c.isDeleted) { - return false; - } - if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { - return true; - } - if (searchText.length >= 8 && c.id.startsWith(searchText)) { - return true; - } - if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) { - return true; - } - if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(searchText) > -1) { - return true; - } - - return false; - }); - } -} diff --git a/libs/angular/src/platform/pipes/fingerprint.pipe.ts b/libs/angular/src/platform/pipes/fingerprint.pipe.ts deleted file mode 100644 index 90289ee212b..00000000000 --- a/libs/angular/src/platform/pipes/fingerprint.pipe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Pipe } from "@angular/core"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { KeyService } from "@bitwarden/key-management"; - -@Pipe({ - name: "fingerprint", - standalone: false, -}) -export class FingerprintPipe { - constructor(private keyService: KeyService) {} - - async transform(publicKey: string | Uint8Array, fingerprintMaterial: string): Promise { - try { - if (typeof publicKey === "string") { - publicKey = Utils.fromB64ToArray(publicKey); - } - - const fingerprint = await this.keyService.getFingerprint(fingerprintMaterial, publicKey); - - if (fingerprint != null) { - return fingerprint.join("-"); - } - - return ""; - } catch { - return ""; - } - } -} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0f857e67247..fea65242346 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -56,6 +56,10 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -385,6 +389,7 @@ import { DefaultStateService, } from "@bitwarden/state-internal"; import { SafeInjectionToken } from "@bitwarden/ui-common"; +import { DefaultUnlockService, UnlockService } from "@bitwarden/unlock"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { PasswordRepromptService } from "@bitwarden/vault"; @@ -915,6 +920,22 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAccountCryptographicStateService, deps: [StateProvider], }), + safeProvider({ + provide: UnlockService, + useClass: DefaultUnlockService, + deps: [ + RegisterSdkService, + AccountCryptographicStateService, + PinStateServiceAbstraction, + KdfConfigService, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + CryptoFunctionServiceAbstraction, + StateProvider, + LogService, + BiometricsService, + ], + }), safeProvider({ provide: BroadcasterService, useClass: DefaultBroadcasterService, @@ -1060,6 +1081,19 @@ const safeProviders: SafeProvider[] = [ PendingAuthRequestsStateService, ], }), + safeProvider({ + provide: AutomaticUserConfirmationService, + useClass: DefaultAutomaticUserConfirmationService, + deps: [ + ConfigService, + ApiServiceAbstraction, + OrganizationUserService, + StateProvider, + InternalOrganizationServiceAbstraction, + OrganizationUserApiService, + InternalPolicyService, + ], + }), safeProvider({ provide: ServerNotificationsService, useClass: devFlagEnabled("noopNotifications") @@ -1079,6 +1113,7 @@ const safeProviders: SafeProvider[] = [ AuthRequestAnsweringService, ConfigService, InternalPolicyService, + AutomaticUserConfirmationService, ], }), safeProvider({ @@ -1181,6 +1216,7 @@ const safeProviders: SafeProvider[] = [ PinServiceAbstraction, KdfConfigService, BiometricsService, + MasterPasswordUnlockService, ], }), safeProvider({ @@ -1510,12 +1546,12 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrganizationMetadataServiceAbstraction, useClass: DefaultOrganizationMetadataService, - deps: [BillingApiServiceAbstraction, ConfigService, PlatformUtilsServiceAbstraction], + deps: [BillingApiServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [StateProvider, PlatformUtilsServiceAbstraction, ApiServiceAbstraction], + deps: [StateProvider], }), safeProvider({ provide: SubscriptionPricingServiceAbstraction, @@ -1667,7 +1703,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, KdfConfigService, KeyService, - SecurityStateService, + AccountCryptographicStateService, ApiServiceAbstraction, StateProvider, ConfigService, diff --git a/libs/assets/src/svg/svgs/index.ts b/libs/assets/src/svg/svgs/index.ts index 1e3000c9b4c..4a2c24454cf 100644 --- a/libs/assets/src/svg/svgs/index.ts +++ b/libs/assets/src/svg/svgs/index.ts @@ -18,6 +18,7 @@ export * from "./empty-trash"; export * from "./favorites.icon"; export * from "./gear"; export * from "./generator"; +export * from "./info-filled.icon"; export * from "./item-types"; export * from "./lock.icon"; export * from "./login-cards"; diff --git a/libs/assets/src/svg/svgs/info-filled.icon.ts b/libs/assets/src/svg/svgs/info-filled.icon.ts new file mode 100644 index 00000000000..6a4d7eb6993 --- /dev/null +++ b/libs/assets/src/svg/svgs/info-filled.icon.ts @@ -0,0 +1,7 @@ +import { svg } from "../svg"; + +export const InfoFilledIcon = svg` + + + +`; diff --git a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html index d66a3a77d93..ed43a32d38c 100644 --- a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html +++ b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.html @@ -1,5 +1,5 @@ - + {{ "yourAccountsFingerprint" | i18n }}: @@ -16,7 +16,7 @@ bitDialogClose > {{ "learnMore" | i18n }} - +
    @@ -78,7 +78,7 @@ [buttonType]="ssoRequired ? 'primary' : 'secondary'" (click)="handleSsoClick()" > - + {{ "useSingleSignOn" | i18n }} @@ -114,7 +114,7 @@ buttonType="secondary" (click)="startAuthRequestLogin()" > - + {{ "loginWithDevice" | i18n }} diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 8e688f3f830..9957c77ffaf 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -52,6 +52,7 @@ import { CheckboxModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, ToastService, TooltipDirective, @@ -79,6 +80,7 @@ export enum LoginUiState { CommonModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, JslibModule, ReactiveFormsModule, diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html index aa6b5c8edc3..031fec5c403 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.html @@ -1,5 +1,5 @@
    - +
    (); diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html index 92c2f9f2f7a..bf40b15b5da 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html @@ -16,11 +16,10 @@ @@ -91,7 +90,7 @@ aria-live="assertive" role="alert" > - {{ "selfHostedEnvFormInvalid" | i18n }} + {{ "selfHostedEnvFormInvalid" | i18n }} diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts index 6fb40179afa..6e093a423b3 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts @@ -27,6 +27,7 @@ import { DialogModule, DialogService, FormFieldModule, + IconModule, LinkModule, TypographyModule, } from "@bitwarden/components"; @@ -85,6 +86,7 @@ function onlyHttpsValidator(): ValidatorFn { JslibModule, DialogModule, ButtonModule, + IconModule, LinkModule, TypographyModule, ReactiveFormsModule, diff --git a/libs/auth/src/angular/sso/sso.component.html b/libs/auth/src/angular/sso/sso.component.html index be38f63987e..9ab11b0d094 100644 --- a/libs/auth/src/angular/sso/sso.component.html +++ b/libs/auth/src/angular/sso/sso.component.html @@ -1,6 +1,6 @@
    - + {{ "loading" | i18n }}
    diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index f5167cb84cc..df358e89107 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -42,6 +42,7 @@ import { CheckboxModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, ToastService, } from "@bitwarden/components"; @@ -73,6 +74,7 @@ interface QueryParams { CommonModule, FormFieldModule, IconButtonModule, + IconModule, LinkModule, JslibModule, ReactiveFormsModule, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html index 6f13b0a1fe2..4c23ab4af5c 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.html @@ -1,6 +1,6 @@
    - +