diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 99efec2fbbb..d1266a174e4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -75,6 +75,7 @@ bitwarden_license/bit-cli/src/admin-console @bitwarden/team-admin-console-dev libs/angular/src/admin-console @bitwarden/team-admin-console-dev libs/common/src/admin-console @bitwarden/team-admin-console-dev libs/admin-console @bitwarden/team-admin-console-dev +libs/auto-confirm @bitwarden/team-admin-console-dev ## Billing team files ## apps/browser/src/billing @bitwarden/team-billing-dev diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index edbc9d98cc9..224020991d1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,27 +9,3 @@ ## 📸 Screenshots - -## ⏰ Reminders before review - -- Contributor guidelines followed -- All formatters and local linters executed and passed -- Written new unit and / or integration tests where applicable -- Protected functional changes with optionality (feature flags) -- Used internationalization (i18n) for all UI strings -- CI builds passed -- Communicated to DevOps any deployment requirements -- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team - -## 🦮 Reviewer guidelines - - - -- 👍 (`:+1:`) or similar for great changes -- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info -- ❓ (`:question:`) for questions -- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion -- 🎨 (`:art:`) for suggestions / improvements -- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention -- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt -- ⛏ (`:pick:`) for minor or nitpick changes diff --git a/.github/renovate.json5 b/.github/renovate.json5 index b402d01e209..1b6522c94dd 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -298,6 +298,7 @@ "oidc-client-ts", "papaparse", "utf-8-validate", + "verifysign", "zxcvbn", ], description: "Tools owned dependencies", diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index b5859516eaa..7614fdba396 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -193,7 +193,7 @@ jobs: zip -r browser-source.zip browser-source - name: Upload browser source - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{matrix.license_type.archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -272,7 +272,7 @@ jobs: npm --version - name: Download browser source - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: ${{matrix.license_type.source_archive_name_prefix}}browser-source-${{ env._BUILD_NUMBER }}.zip @@ -336,7 +336,7 @@ jobs: working-directory: browser-source/apps/browser - name: Upload extension artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name }}-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name }} @@ -349,7 +349,7 @@ jobs: - name: Upload dev extension artifact if: ${{ matrix.browser.archive_name_dev != '' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ matrix.license_type.artifact_prefix }}${{ matrix.browser.artifact_name_dev }}-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/${{matrix.license_type.archive_name_prefix}}${{ matrix.browser.archive_name_dev }} @@ -523,7 +523,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{matrix.license_type.archive_name_prefix}}dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/${{matrix.license_type.archive_name_prefix}}dist-safari.zip diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 704a9810b27..d0abe8e12e7 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -268,7 +268,7 @@ jobs: fi - name: Upload unix zip asset - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip @@ -482,7 +482,7 @@ jobs: } - name: Upload windows zip asset - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip @@ -490,7 +490,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -503,7 +503,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -535,7 +535,7 @@ jobs: echo "BW Package Version: $_PACKAGE_VERSION" - name: Get bw linux cli - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap @@ -572,7 +572,7 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 8e43127770c..701e6208b60 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -57,7 +57,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Verify @@ -90,7 +90,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: true - name: Get Package Version @@ -176,7 +176,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Free disk space @@ -261,42 +261,42 @@ jobs: run: npm run dist:lin - name: Upload tar.gz artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_x64.tar.gz path: apps/desktop/dist/bitwarden_desktop_x64.tar.gz if-no-files-found: error - name: Upload .deb artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -309,7 +309,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: com.bitwarden.desktop.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -335,7 +335,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Set up Node @@ -437,14 +437,14 @@ jobs: run: npm run dist:lin:arm64 - name: Upload .snap artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_arm64.snap if-no-files-found: error - name: Upload tar.gz artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz @@ -457,7 +457,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: com.bitwarden.desktop-arm64.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -483,7 +483,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Set up Node @@ -630,7 +630,7 @@ jobs: -NewName bitwarden-$env:_PACKAGE_VERSION-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe @@ -638,7 +638,7 @@ jobs: - name: Upload installer exe artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe @@ -646,7 +646,7 @@ jobs: - name: Upload appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -654,7 +654,7 @@ jobs: - name: Upload store appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -662,7 +662,7 @@ jobs: - name: Upload NSIS ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -670,7 +670,7 @@ jobs: - name: Upload appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx @@ -678,7 +678,7 @@ jobs: - name: Upload store appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -686,7 +686,7 @@ jobs: - name: Upload NSIS x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -694,7 +694,7 @@ jobs: - name: Upload appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -702,7 +702,7 @@ jobs: - name: Upload store appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -710,7 +710,7 @@ jobs: - name: Upload NSIS ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -718,7 +718,7 @@ jobs: - name: Upload nupkg artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg @@ -726,7 +726,7 @@ jobs: - name: Upload auto-update artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -883,7 +883,7 @@ jobs: -NewName latest-beta.yml - name: Upload portable exe artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe @@ -891,7 +891,7 @@ jobs: - name: Upload installer exe artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe @@ -899,7 +899,7 @@ jobs: - name: Upload appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -907,7 +907,7 @@ jobs: - name: Upload store appx ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -915,7 +915,7 @@ jobs: - name: Upload NSIS ia32 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -923,7 +923,7 @@ jobs: - name: Upload appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx @@ -931,7 +931,7 @@ jobs: - name: Upload store appx x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -939,7 +939,7 @@ jobs: - name: Upload NSIS x64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -947,7 +947,7 @@ jobs: - name: Upload appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -955,7 +955,7 @@ jobs: - name: Upload store appx ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -963,7 +963,7 @@ jobs: - name: Upload NSIS ARM64 artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -971,7 +971,7 @@ jobs: - name: Upload auto-update artifact if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: latest-beta.yml path: apps/desktop/dist/nsis-web/latest-beta.yml @@ -996,7 +996,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Set up Node @@ -1236,7 +1236,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Set up Node @@ -1429,7 +1429,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1462,28 +1462,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -1511,7 +1511,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Set up Node @@ -1712,7 +1712,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1755,14 +1755,14 @@ jobs: $buildInfo | ConvertTo-Json | Set-Content -Path dist/macos-build-number.json - name: Upload MacOS App Store build number artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: macos-build-number.json path: apps/desktop/dist/macos-build-number.json if-no-files-found: error - name: Upload .pkg artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1852,7 +1852,7 @@ jobs: - name: Check out repo uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Log in to Azure @@ -1884,6 +1884,328 @@ jobs: upload_sources: true upload_translations: false + validate-linux-x64-deb: + name: Validate Linux x64 .deb + runs-on: ubuntu-22.04 + needs: + - setup + - linux + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download deb artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/linux/deb + artifacts: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libasound2 xvfb + + - name: Install .deb + working-directory: apps/desktop/artifacts/linux/deb + run: sudo apt-get install -y ./Bitwarden-${_PACKAGE_VERSION}-amd64.deb + + - name: Run .deb + run: | + xvfb-run -a bitwarden & + sleep 30 + if pgrep bitwarden > /dev/null; then + pkill -9 bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-linux-x64-appimage: + name: Validate Linux x64 appimage + runs-on: ubuntu-22.04 + needs: + - setup + - linux + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download appimage artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/linux/appimage + artifacts: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libasound2 libfuse2 xvfb + + - name: Run AppImage + working-directory: apps/desktop/artifacts/linux/appimage + run: | + chmod a+x ./Bitwarden-${_PACKAGE_VERSION}-x86_64.AppImage + xvfb-run -a ./Bitwarden-${_PACKAGE_VERSION}-x86_64.AppImage --no-sandbox & + sleep 30 + if pgrep bitwarden > /dev/null; then + pkill -9 bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-linux-wayland: + name: Validate Linux Wayland + runs-on: ubuntu-22.04 + needs: + - setup + - linux + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download appimage artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/linux/appimage + artifacts: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libasound2 libfuse2 xvfb + sudo apt-get install -y weston libwayland-client0 libwayland-server0 libwayland-dev + + - name: Run headless Wayland compositor + run: | + # Start Weston in a virtual terminal in headless mode + weston --headless --socket=wayland-0 & + # Let the compositor start + sleep 5 + + - name: Run AppImage + working-directory: apps/desktop/artifacts/linux/appimage + env: + WAYLAND_DISPLAY: wayland-0 + run: | + chmod a+x ./Bitwarden-${_PACKAGE_VERSION}-x86_64.AppImage + xvfb-run -a ./Bitwarden-${_PACKAGE_VERSION}-x86_64.AppImage --no-sandbox & + sleep 30 + if pgrep bitwarden > /dev/null; then + pkill -9 bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-linux-flatpak: + name: Validate Linux ${{ matrix.os }} Flatpak + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + strategy: + matrix: + os: + - ubuntu-22.04 + - ubuntu-22.04-arm + needs: + - setup + - linux + - linux-arm64 + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download flatpak artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/linux/flatpak/ + artifacts: com.bitwarden.${{ matrix.os == 'ubuntu-22.04' && 'desktop' || 'desktop-arm64' }}.flatpak + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libasound2 flatpak xvfb dbus-x11 + flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak install -y --user flathub + + - name: Install flatpak + working-directory: apps/desktop/artifacts/linux/flatpak + run: flatpak install -y --user --bundle com.bitwarden.desktop.flatpak + + - name: Run Flatpak + run: | + export $(dbus-launch) + xvfb-run -a flatpak run com.bitwarden.desktop & + sleep 30 + if pgrep bitwarden > /dev/null; then + pkill -9 bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-linux-snap: + name: Validate Linux ${{ matrix.os }} Snap + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + strategy: + matrix: + os: + - ubuntu-22.04 + - ubuntu-22.04-arm + needs: + - setup + - linux + - linux-arm64 + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + _CPU_ARCH: ${{ matrix.os == 'ubuntu-22.04' && 'amd64' || 'arm64' }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download snap artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/linux/snap + artifacts: bitwarden_${{ env._PACKAGE_VERSION }}_${{ env._CPU_ARCH }}.snap + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y libasound2 snapd xvfb + + - name: Install snap + working-directory: apps/desktop/artifacts/linux/snap + run: | + sudo snap install --dangerous ./bitwarden_${_PACKAGE_VERSION}_${_CPU_ARCH}.snap + + - name: Run Snap + run: | + xvfb-run -a snap run bitwarden & + sleep 30 + if pgrep bitwarden > /dev/null; then + pkill -9 bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-macos-dmg: + name: Validate MacOS dmg + runs-on: macos-15 + needs: + - setup + - macos-package-github + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download dmg artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/macos/dmg + artifacts: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg + + - name: Install dmg + working-directory: apps/desktop/artifacts/macos/dmg + run: | + # mount + hdiutil attach Bitwarden-${_PACKAGE_VERSION}-universal.dmg + # install + cp -r /Volumes/Bitwarden\ ${_PACKAGE_VERSION}-universal/Bitwarden.app /Applications/ + # unmount + hdiutil detach /Volumes/Bitwarden\ ${_PACKAGE_VERSION}-universal + + - name: Run dmg + run: | + open /Applications/Bitwarden.app/Contents/MacOS/Bitwarden & + sleep 30 + if pgrep Bitwarden > /dev/null; then + pkill -9 Bitwarden + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + + validate-windows-portable: + name: Validate Windows portable + runs-on: windows-2022 + needs: + - setup + - windows + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + steps: + - name: Check out repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Download portable artifact + uses: bitwarden/gh-actions/download-artifacts@main + with: + run_id: ${{ github.run_id }} + path: apps/desktop/artifacts/windows/portable + artifacts: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe + + - name: Run Portable exe + working-directory: apps/desktop/artifacts/windows/portable + run: | + "./Bitwarden-Portable-$_PACKAGE_VERSION.exe" --no-sandbox & + sleep 30 + if tasklist | grep Bitwarden ; then + taskkill //F //IM "Bitwarden.exe" + echo "Bitwarden is running." + else + echo "Bitwarden is not running." + exit 1 + fi + check-failures: name: Check for failures if: always() @@ -1898,6 +2220,13 @@ jobs: - macos-package-github - macos-package-mas - crowdin-push + - validate-linux-x64-deb + - validate-linux-x64-appimage + - validate-linux-flatpak + - validate-linux-snap + - validate-linux-wayland + - validate-macos-dmg + - validate-windows-portable permissions: contents: read id-token: write diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 7d302fb453b..24a8df084a2 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -307,7 +307,7 @@ jobs: zip -r web-$_VERSION-${{ matrix.artifact_name }}.zip build - name: Upload ${{ matrix.artifact_name }} artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3aeb75dcbf6..83c931b4fe0 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@6d653acede28d24f02e3cd41383119e8b1b35921 # stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: toolchain: stable components: rustfmt, clippy - name: Install Rust nightly - uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: toolchain: nightly components: rustfmt @@ -115,7 +115,7 @@ jobs: run: rustup --version - name: Cache cargo registry - uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: Run cargo fmt working-directory: ./apps/desktop/desktop_native @@ -128,7 +128,7 @@ jobs: RUSTFLAGS: "-D warnings" - name: Install cargo-sort - run: cargo install cargo-sort --locked --git https://github.com/DevinR528/cargo-sort.git --rev f5047967021cbb1f822faddc355b3b07674305a1 + run: cargo install cargo-sort --locked --git https://github.com/DevinR528/cargo-sort.git --rev ac6e328faf467a39e38ab48dc60dcf4f6a46d7a5 # v2.0.2 - name: Cargo sort working-directory: ./apps/desktop/desktop_native diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml index 1e23c31b033..3a7431c07f0 100644 --- a/.github/workflows/nx.yml +++ b/.github/workflows/nx.yml @@ -36,7 +36,7 @@ jobs: run: npm ci - name: Set Nx SHAs for affected detection - uses: nrwl/nx-set-shas@826660b82addbef3abff5fa871492ebad618c9e1 # v4.3.3 + uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 - name: Run Nx affected tasks continue-on-error: true diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index b2edf0171db..79f3335313e 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -71,6 +71,8 @@ jobs: version_web: ${{ steps.set-final-version-output.outputs.version_web }} permissions: id-token: write + contents: write + pull-requests: write steps: - name: Validate version input format @@ -93,6 +95,13 @@ jobs: keyvault: gh-org-bitwarden secrets: "BW-GHAPP-ID,BW-GHAPP-KEY" + - name: Retrieve GPG secrets + id: retrieve-gpg-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-gpg-private-key, github-gpg-private-key-passphrase" + - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main @@ -102,7 +111,8 @@ jobs: with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - permission-contents: write # for committing and pushing to current branch + permission-contents: write # for creating, committing to, and pushing new branches + permission-pull-requests: write # for generating pull requests - name: Check out branch uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -113,8 +123,20 @@ jobs: - name: Configure Git run: | - git config --local user.email "actions@github.com" - git config --local user.name "Github Actions" + git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" + git config --local user.name "bitwarden-devops-bot" + + - name: Setup GPG signing + env: + GPG_PRIVATE_KEY: ${{ steps.retrieve-gpg-secrets.outputs.github-gpg-private-key }} + GPG_PASSPHRASE: ${{ steps.retrieve-gpg-secrets.outputs.github-gpg-private-key-passphrase }} + run: | + echo "$GPG_PRIVATE_KEY" | gpg --import --batch + GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep -o "rsa[0-9]\+/[A-F0-9]\+" | head -n1 | cut -d'/' -f2) + git config --local user.signingkey "$GPG_KEY_ID" + git config --local commit.gpgsign true + export GPG_TTY=$(tty) + echo "test" | gpg --clearsign --pinentry-mode=loopback --passphrase "$GPG_PASSPHRASE" > /dev/null 2>&1 ######################## # VERSION BUMP SECTION # @@ -426,13 +448,53 @@ jobs: echo "No changes to commit!"; fi - - name: Commit files + - name: Create version bump branch if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - run: git commit -m "Bumped client version(s)" -a + run: | + BRANCH_NAME="version-bump-$(date +%s)" + git checkout -b "$BRANCH_NAME" + echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV - - name: Push changes + - name: Commit version bumps with GPG signature if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} - run: git push + run: | + git commit -m "Bumped client version(s)" -a + + - name: Push version bump branch + if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} + run: | + git push --set-upstream origin "$BRANCH_NAME" + + - name: Create Pull Request for version bump + if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + VERSION_BROWSER: ${{ steps.set-final-version-output.outputs.version_browser }} + VERSION_CLI: ${{ steps.set-final-version-output.outputs.version_cli }} + VERSION_DESKTOP: ${{ steps.set-final-version-output.outputs.version_desktop }} + VERSION_WEB: ${{ steps.set-final-version-output.outputs.version_web }} + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const versions = []; + if (process.env.VERSION_BROWSER) versions.push(`- Browser: ${process.env.VERSION_BROWSER}`); + if (process.env.VERSION_CLI) versions.push(`- CLI: ${process.env.VERSION_CLI}`); + if (process.env.VERSION_DESKTOP) versions.push(`- Desktop: ${process.env.VERSION_DESKTOP}`); + if (process.env.VERSION_WEB) versions.push(`- Web: ${process.env.VERSION_WEB}`); + + const body = versions.length > 0 + ? `Automated version bump:\n\n${versions.join('\n')}` + : 'Automated version bump'; + + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Bumped client version(s)', + body: body, + head: process.env.BRANCH_NAME, + base: context.ref.replace('refs/heads/', '') + }); + console.log(`Created PR #${pr.number}: ${pr.html_url}`); cut_branch: name: Cut branch diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8f062ea345..cf7251b259a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: run: npm test -- --coverage --maxWorkers=3 - name: Report test results - uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 + uses: dorny/test-reporter@7b7927aa7da8b82e81e755810cb51f39941a2cc7 # v2.2.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results @@ -74,7 +74,7 @@ jobs: uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 - name: Upload test coverage - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: jest-coverage path: ./coverage/lcov.info @@ -142,7 +142,7 @@ jobs: persist-credentials: false - name: Install rust - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: toolchain: stable components: llvm-tools @@ -160,7 +160,7 @@ jobs: run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage - name: Upload test coverage - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: rust-coverage path: ./apps/desktop/desktop_native/lcov.info @@ -178,13 +178,13 @@ jobs: persist-credentials: false - name: Download jest coverage - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: jest-coverage path: ./ - name: Download rust coverage - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: rust-coverage path: ./apps/desktop/desktop_native diff --git a/.storybook/main.ts b/.storybook/main.ts index e1f3561a1b7..353d959a6b9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -12,6 +12,8 @@ const config: StorybookConfig = { "../libs/dirt/card/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/pricing/src/**/*.mdx", "../libs/pricing/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/subscription/src/**/*.mdx", + "../libs/subscription/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/tools/send/send-ui/src/**/*.mdx", "../libs/tools/send/send-ui/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/vault/src/**/*.mdx", diff --git a/apps/browser/spec/mock-port.spec-util.ts b/apps/browser/spec/mock-port.spec-util.ts index 39239ba8817..427a0b3aa9c 100644 --- a/apps/browser/spec/mock-port.spec-util.ts +++ b/apps/browser/spec/mock-port.spec-util.ts @@ -12,7 +12,10 @@ export function mockPorts() { (chrome.runtime.connect as jest.Mock).mockImplementation((portInfo) => { const port = mockDeep(); port.name = portInfo.name; - port.sender = { url: chrome.runtime.getURL("") }; + port.sender = { + url: chrome.runtime.getURL(""), + origin: chrome.runtime.getURL(""), + }; // convert to internal port delete (port as any).tab; diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 25cc1e31425..3b9ca37f6b4 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "تعديل" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "خيارات تسجيل الدخول بخطوتين المملوكة لجهات اخرى مثل YubiKey و Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "نظافة كلمة المرور، صحة الحساب، وتقارير تسريبات البيانات للحفاظ على سلامة خزانتك." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index d7257bab478..5eeedc430b1 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Element arxivə göndərildi" }, + "itemWasUnarchived": { + "message": "Element arxivdən çıxarıldı" + }, "itemUnarchived": { "message": "Element arxivdən çıxarıldı" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Arxivlənmiş elementlər ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək. Bu elementi arxivləmək istədiyinizə əminsiniz?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Arxivi istifadə etmək üçün premium üzvlük tələb olunur." }, + "itemRestored": { + "message": "Element bərpa edildi" + }, "edit": { "message": "Düzəliş et" }, @@ -1251,7 +1263,7 @@ "description": "Detailed error message shown when saving login details fails." }, "changePasswordWarning": { - "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saat ərzində çıxış sonlandırılacaq." + "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saat ərzində sonlandırılacaq." }, "accountRecoveryUpdateMasterPasswordSubtitle": { "message": "Hesabın geri qaytarılması prosesini tamamlamaq üçün ana parolunuzu dəyişdirin." @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və veri pozuntusu hesabatları." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Konsolu" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Avtomatik istifadəçi təsdiqi" + }, + "automaticUserConfirmationHint": { + "message": "Bu cihazın kilidi açıq olduqda gözləyən istifadəçiləri avtomatik təsdiqlə" + }, + "autoConfirmOnboardingCallout": { + "message": "Avtomatik istifadəçi təsdiqi ilə vaxta qənaət edin" + }, + "autoConfirmWarning": { + "message": "Bu, təşkilatınızın veri təhlükəsizliyinə təsir edə bilər. " + }, + "autoConfirmWarningLink": { + "message": "Risklər barədə öyrən" + }, + "autoConfirmSetup": { + "message": "Yeni istifadəçiləri avtomatik təsdiqlə" + }, + "autoConfirmSetupDesc": { + "message": "Bu cihazın kilidi açıq olduqda yeni istifadəçilər avtomatik təsdiqlənəcək." + }, + "autoConfirmSetupHint": { + "message": "Potensial təhlükəsizlik riskləri nələrdir?" + }, + "autoConfirmEnabled": { + "message": "Avtomatik təsdiq işə salındı" + }, + "availableNow": { + "message": "İndi mmövcuddur" + }, "accountSecurity": { "message": "Hesab güvənliyi" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu giriş risk altındadır və bir veb sayt əskikdir. Daha güclü təhlükəsizlik üçün bir veb sayt əlavə edin və parolu dəyişdirin." }, + "vulnerablePassword": { + "message": "Zəifliyi olan parol." + }, + "changeNow": { + "message": "İndi dəyişdir" + }, "missingWebsite": { "message": "Əskik veb sayt" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Bunu niyə görürəm?" + }, + "resizeSideNavigation": { + "message": "Yan naviqasiyanı yeni. ölçüləndir" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index fd4dbb780da..96042f12e19 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Рэдагаваць" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Прапрыетарныя варыянты двухэтапнага ўваходу, такія як YubiKey і Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Кансоль адміністратара" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Бяспеке акаўнта" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 1dc20d6ab65..9a420b1d177 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Елементът беше преместен в архива" }, + "itemWasUnarchived": { + "message": "Елементът беше изваден от архива" + }, "itemUnarchived": { "message": "Елементът беше изваден от архива" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Архивираните елементи са изключени от общите резултати при търсене и от предложенията за автоматично попълване. Наистина ли искате да архивирате този елемент?" }, + "archived": { + "message": "Архивирано" + }, + "unarchiveAndSave": { + "message": "Разархивиране и запазване" + }, "upgradeToUseArchive": { "message": "За да се възползвате от архивирането, трябва да ползвате платен абонамент." }, + "itemRestored": { + "message": "Записът бе възстановен" + }, "edit": { "message": "Редактиране" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Частно двустепенно удостоверяване чрез YubiKey и Duo." }, + "premiumSubscriptionEnded": { + "message": "Вашият абонамент за платения план е приключил" + }, + "archivePremiumRestart": { + "message": "Ако искате отново да получите достъп до архива си, трябва да подновите платения си абонамент. Ако редактирате данните за архивиран елемент преди подновяването, той ще бъде върнат в трезора." + }, + "restartPremium": { + "message": "Подновяване на платения абонамент" + }, "ppremiumSignUpReports": { "message": "Проверки в списъците с публикувани пароли, проверка на регистрациите и доклади за пробивите в сигурността, което спомага трезорът ви да е допълнително защитен." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Административна конзола" }, + "admin": { + "message": "Администратор" + }, + "automaticUserConfirmation": { + "message": "Автоматично потвърждение на потребителите" + }, + "automaticUserConfirmationHint": { + "message": "Автоматично потвърждение на потребителите, когато това устройство е отключено" + }, + "autoConfirmOnboardingCallout": { + "message": "Спестете време с автоматичното потвърждение на потребителите" + }, + "autoConfirmWarning": { + "message": "Това може да се отрази на сигурността на данните в организацията Ви. " + }, + "autoConfirmWarningLink": { + "message": "Научете повече за рисковете" + }, + "autoConfirmSetup": { + "message": "Автоматично потвърждаване на новите потребители" + }, + "autoConfirmSetupDesc": { + "message": "Новите потребители ще бъдат потвърждавани автоматично, докато това устройство е отключено." + }, + "autoConfirmSetupHint": { + "message": "Какви са възможните рискове за сигурността?" + }, + "autoConfirmEnabled": { + "message": "Автоматичното потвърждаване е включено" + }, + "availableNow": { + "message": "Налично сега" + }, "accountSecurity": { "message": "Защита на регистрацията" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Този елемент за вписване е в риск и в него липсва уеб сайт. Добавете уеб сайт и сменете паролата, за по-добра сигурност." }, + "vulnerablePassword": { + "message": "Уязвима парола." + }, + "changeNow": { + "message": "Промяна сега" + }, "missingWebsite": { "message": "Липсващ уеб сайт" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Защо виждам това?" + }, + "resizeSideNavigation": { + "message": "Преоразмеряване на страничната навигация" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index baf98bcb50a..78b55611b50 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "সম্পাদনা" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "আপনার ভল্টটি সুরক্ষিত রাখতে পাসওয়ার্ড স্বাস্থ্যকরন, অ্যাকাউন্ট স্বাস্থ্য এবং ডেটা লঙ্ঘনের প্রতিবেদন।" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index df473b0192d..3bd578aa2a3 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 4bbfb9a418e..97b9911536a 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edita" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Requisits d'higiene de la contrasenya, salut del compte i informe d'infraccions de dades per mantenir la seguretat de la vostra caixa forta." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Consola d'administració" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Seguretat del compte" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index e40edbb8091..4c88374ed3e 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Položka byla přesunuta do archivu" }, + "itemWasUnarchived": { + "message": "Položka byla odebrána z archivu" + }, "itemUnarchived": { "message": "Položka byla odebrána z archivu" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archivované položky jsou vyloučeny z obecných výsledků vyhledávání a z návrhů automatického vyplňování. Jste si jisti, že chcete tuto položku archivovat?" }, + "archived": { + "message": "Archivováno" + }, + "unarchiveAndSave": { + "message": "Odebrat z archivu a uložit" + }, "upgradeToUseArchive": { "message": "Pro použití funkce Archiv je potřebné prémiové členství." }, + "itemRestored": { + "message": "Položka byla obnovena" + }, "edit": { "message": "Upravit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo." }, + "premiumSubscriptionEnded": { + "message": "Vaše předplatné Premium skončilo" + }, + "archivePremiumRestart": { + "message": "Chcete-li získat přístup k Vašemu archivu, restartujte předplatné Premium. Pokud upravíte detaily archivované položky před restartováním, bude přesunuta zpět do Vašeho trezoru." + }, + "restartPremium": { + "message": "Restartovat Premium" + }, "ppremiumSignUpReports": { "message": "Reporty o hygieně Vašich hesel, zdraví účtu a narušeních bezpečnosti." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Konzole správce" }, + "admin": { + "message": "Administrátor" + }, + "automaticUserConfirmation": { + "message": "Automatické potvrzení uživatele" + }, + "automaticUserConfirmationHint": { + "message": "Automaticky potvrdit čekající uživatele, když je toto zařízení odemčeno" + }, + "autoConfirmOnboardingCallout": { + "message": "Ušetřete čas s automatickým potvrzením uživatele" + }, + "autoConfirmWarning": { + "message": "To by mohlo ovlivnit bezpečnost dat Vaší organizace. " + }, + "autoConfirmWarningLink": { + "message": "Více o rizicích" + }, + "autoConfirmSetup": { + "message": "Automaticky potvrdit nové uživatele" + }, + "autoConfirmSetupDesc": { + "message": "Noví uživatelé budou automaticky potvrzeni, když bude toto zařízení odemčeno." + }, + "autoConfirmSetupHint": { + "message": "Jaká jsou možná bezpečnostní rizika?" + }, + "autoConfirmEnabled": { + "message": "Zapnuto automatické potvrzení" + }, + "availableNow": { + "message": "Nyní k dispozici" + }, "accountSecurity": { "message": "Zabezpečení účtu" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Tyto přihlašovací údaje jsou ohrožené a chybí jim webová stránka. Přidejte webovou stránku a změňte heslo pro větší bezpečnost." }, + "vulnerablePassword": { + "message": "Zranitelné heslo." + }, + "changeNow": { + "message": "Změnit nyní" + }, "missingWebsite": { "message": "Chybějící webová stránka" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Proč se mi toto zobrazuje?" + }, + "resizeSideNavigation": { + "message": "Změnit velikost boční navigace" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 896eee5af4f..64a240add67 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -344,16 +344,16 @@ "message": "Bitwarden for Business" }, "bitwardenAuthenticator": { - "message": "Dilyswr Bitwarden" + "message": "Dilysydd Bitwarden" }, "continueToAuthenticatorPageDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Rheolydd Cyfrinachau Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Gallwch storio, rheoli a rhannu cyfrinachau datblygwyr yn ddiogel gyda Rheolydd Cyfrinachau Bitwarden. Dysgwch fwy ar wefan bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Golygu" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Dewisiadau mewngofnodi dau gam perchenogol megis YubiKey a Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4152,7 +4173,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "Datglowch eich cyfrif i weld argymhellion llenwi awtomatig", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Diogelwch eich cyfrif" }, @@ -4936,7 +4990,7 @@ "message": "Download Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Lawrlwytho Bitwarden ar bob dyfais" }, "getTheMobileApp": { "message": "Get the mobile app" @@ -4966,7 +5020,7 @@ "message": "Premium" }, "unlockFeaturesWithPremium": { - "message": "Unlock reporting, emergency access, and more security features with Premium." + "message": "Datglowch nodweddion diogelwch megis adroddiadau, mynediad mewn argyfwng, a mwy drwy gyfrif Premium." }, "freeOrgsCannotUseAttachments": { "message": "Free organizations cannot use attachments" @@ -4975,7 +5029,7 @@ "message": "Filters" }, "filterVault": { - "message": "Filter vault" + "message": "Hidlo'r gell" }, "filterApplied": { "message": "One filter applied" @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -5855,7 +5915,7 @@ "message": "Great job securing your at-risk logins!" }, "upgradeNow": { - "message": "Upgrade now" + "message": "Uwchraddio nawr" }, "builtInAuthenticator": { "message": "Built-in authenticator" @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index ad6e5b0e90d..640dbaf89b7 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Redigér" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Adgangskodehygiejne, kontosundhed og rapporter om datalæk til at holde din boks sikker." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin-konsol" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Kontosikkerhed" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 5ba6dd419a1..ce72944943c 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Eintrag wurde archiviert" }, + "itemWasUnarchived": { + "message": "Eintrag wird nicht mehr archiviert" + }, "itemUnarchived": { "message": "Eintrag wird nicht mehr archiviert" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archivierte Einträge werden von allgemeinen Suchergebnissen sowie Vorschlägen zum automatischen Ausfüllen ausgeschlossen. Bist du sicher, dass du diesen Eintrag archivieren möchtest?" }, + "archived": { + "message": "Archiviert" + }, + "unarchiveAndSave": { + "message": "Nicht mehr archivieren und speichern" + }, "upgradeToUseArchive": { "message": "Für die Nutzung des Archivs ist eine Premium-Mitgliedschaft erforderlich." }, + "itemRestored": { + "message": "Eintrag wurde wiederhergestellt" + }, "edit": { "message": "Bearbeiten" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo." }, + "premiumSubscriptionEnded": { + "message": "Dein Premium-Abonnement ist abgelaufen" + }, + "archivePremiumRestart": { + "message": "Starte dein Premium-Abonnement neu, um den Zugriff auf dein Archiv wiederherzustellen. Wenn du die Details für einen archivierten Eintrag vor dem Neustart bearbeitest, wird er wieder zurück in deinen Tresor verschoben." + }, + "restartPremium": { + "message": "Premium neu starten" + }, "ppremiumSignUpReports": { "message": "Berichte über Kennworthygiene, Kontostatus und Datenschutzverletzungen, um deinen Tresor sicher zu halten." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Administrator-Konsole" }, + "admin": { + "message": "Administrator" + }, + "automaticUserConfirmation": { + "message": "Automatische Benutzerbestätigung" + }, + "automaticUserConfirmationHint": { + "message": "Ausstehende Benutzer automatisch bestätigen, während dieses Gerät entsperrt ist" + }, + "autoConfirmOnboardingCallout": { + "message": "Spare Zeit durch die automatische Benutzerbestätigung" + }, + "autoConfirmWarning": { + "message": "Dies könnte die Datensicherheit deiner Organisation beeinflussen. " + }, + "autoConfirmWarningLink": { + "message": "Erfahre mehr über die Risiken" + }, + "autoConfirmSetup": { + "message": "Neue Benutzer automatisch bestätigen" + }, + "autoConfirmSetupDesc": { + "message": "Neue Benutzer werden automatisch bestätigt, während dieses Gerät entsperrt ist." + }, + "autoConfirmSetupHint": { + "message": "Was sind die möglichen Sicherheitsrisiken?" + }, + "autoConfirmEnabled": { + "message": "Automatische Bestätigung aktiviert" + }, + "availableNow": { + "message": "Jetzt verfügbar" + }, "accountSecurity": { "message": "Kontosicherheit" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, + "vulnerablePassword": { + "message": "Gefährdetes Passwort." + }, + "changeNow": { + "message": "Jetzt ändern" + }, "missingWebsite": { "message": "Fehlende Website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Warum wird mir das angezeigt?" + }, + "resizeSideNavigation": { + "message": "Größe der Seitennavigation ändern" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 29593380dd9..f20edf63793 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Επεξεργασία" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγεία λογαριασμού και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλές το vault σας." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Κονσόλα Διαχειριστή" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Ασφάλεια λογαριασμού" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 95d3f662994..36ba57cb7e8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4718,6 +4739,9 @@ } } }, + "moreOptionsLabelNoPlaceholder": { + "message": "More options" + }, "moreOptionsTitle": { "message": "More options - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", @@ -4808,6 +4832,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin" :{ + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout":{ + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5071,14 +5128,11 @@ } } }, - "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", - "placeholders": { - "website": { - "content": "$1", - "example": "https://example.com" - } - } + "showMatchDetectionNoPlaceholder": { + "message": "Show match detection" + }, + "hideMatchDetectionNoPlaceholder": { + "message": "Hide match detection" }, "autoFillOnPageLoad": { "message": "Autofill on page load?" @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6018,7 +6078,7 @@ "placeholders": { "organization": { "content": "$1", - "example": "My Org Name" + "example": "My Org Name" } } }, @@ -6027,7 +6087,7 @@ "placeholders": { "organization": { "content": "$1", - "example": "My Org Name" + "example": "My Org Name" } } }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index f64bf387a5e..4fd70672f4a 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organisation’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 34f2f0b1105..a92aac915c1 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organisation’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 276d0dbedb2..f72635696a8 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Editar" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opciones de inicio de sesión con autenticación de dos pasos propietarios como YubiKey y Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Higiene de contraseña, salud de la cuenta e informes de violaciones de datos para mantener su caja fuerte segura." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Consola de administrador" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Seguridad de la cuenta" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 3efb771966d..a7730b536f7 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Muuda" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Parooli hügieen, konto seisukord ja andmelekete raportid aitavad hoidlat turvalisena hoida." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index d8f8c5230c6..7a6ca24b3da 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Editatu" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Pasahitzaren higienea, kontuaren egoera eta datu-bortxaketen txostenak, kutxa gotorra seguru mantentzeko." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index c264b292761..c5af707763f 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "ویرایش" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "گزینه‌های ورود اضافی دو مرحله‌ای مانند YubiKey و Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "گزارش‌های بهداشت کلمه عبور، سلامت حساب کاربری و نقض داده‌ها برای ایمن نگهداشتن گاوصندوق شما." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "کنسول مدیر" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "امنیت حساب کاربری" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 38003bb31b2..cfab00e0849 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Muokkaa" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Kaksivaiheisen kirjautumisen erikoisvaihtoehdot, kuten YubiKey ja Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Salasanahygienian, tilin terveyden ja tietovuotojen raportointitoiminnot pitävät holvisi turvassa." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Hallintapaneelista" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Tilin suojaus" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 38702b940c4..91fe7f70ced 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "I-edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Pagmamay-ari na dalawang hakbang na opsyon sa pag-log in gaya ng YubiKey at Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Pasahod higiyena, kalusugan ng account, at mga ulat sa data breach upang panatilihing ligtas ang iyong vault." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index afc71011900..e884c3cd141 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "L'élément a été envoyé à l'archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "L'élément a été désarchivé" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Les éléments archivés sont exclus des résultats de recherche généraux et des suggestions de remplissage automatique. Êtes-vous sûr de vouloir archiver cet élément ?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Une adhésion premium est requise pour utiliser Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Modifier" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart 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." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Console Admin" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Sécurité du compte" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "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." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Site Web manquant" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 7bfe70bded5..27c66bb1ce6 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Editar" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opcións de verificación en 2 pasos privadas tales coma YubiKey ou Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Limpeza de contrasinais, saúde de contas e informes de filtración de datos para manter a túa caixa forte segura." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Consola do administrador" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Seguridade da conta" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index fd4e010ce60..8c09d562f60 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "הפריט נשלח לארכיון" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "הפריט הוסר מהארכיון" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "פריטים בארכיון מוחרגים מתוצאות חיפוש כללי והצעות למילוי אוטומטי. האם אתה בטוח שברצונך להעביר פריט זה לארכיון?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "ערוך" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "אפשרויות כניסה דו־שלבית קנייניות כגון YubiKey ו־Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "היגיינת סיסמאות, מצב בריאות החשבון, ודיווחים מעודכנים על פרצות חדשות בכדי לשמור על הכספת שלך בטוחה." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "מסוף מנהל" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "אבטחת החשבון" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "כניסה זו נמצאת בסיכון וחסר בה אתר אינטרנט. הוסף אתר אינטרנט ושנה את הסיסמה לאבטחה חזקה יותר." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "לא נמצא אתר אינטרנט" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index dc91aa89197..50fc056527a 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "संपादन करें" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "अपनी वॉल्ट को सुरक्षित रखने के लिए पासवर्ड स्वच्छता, खाता स्वास्थ्य और डेटा उल्लंघन रिपोर्ट।" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index c472bfa50cd..3e94ef40a30 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Stavka poslana u arhivu" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Stavka vraćena iz arhive" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Uredi" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Mogućnosti za prijavu u dva koraka kao što su YubiKey i Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Higijenu lozinki, zdravlje računa i izvještaje o krađi podatak radi zaštite svojeg trezora." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Konzola administratora" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Sigurnost računa" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ova prijava je ugrožena i nedostaje joj web-stranica. Dodaj web-stranicu i promijeni lozinku za veću sigurnost." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Nedostaje web-stranica" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 438c574bba9..3977f7a4fb5 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Az elem az archivumba került." }, + "itemWasUnarchived": { + "message": "Az elem visszavételre került az archivumból." + }, "itemUnarchived": { "message": "Az elemek visszavéelre kerültek az archivumból." }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Az archivált elemek ki vannak zárva az általános keresési eredményekből és az automatikus kitöltési javaslatokból. Biztosan archiválni szeretnénk ezt az elemet?" }, + "archived": { + "message": "Archiválva" + }, + "unarchiveAndSave": { + "message": "Archiválás visszavonása és mentés" + }, "upgradeToUseArchive": { "message": "Az Archívum használatához prémium tagság szükséges." }, + "itemRestored": { + "message": "Az elem visszaállításra került." + }, "edit": { "message": "Szerkesztés" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Saját kétlépcsős bejelentkezési lehetőségek mint a YubiKey és a Duo." }, + "premiumSubscriptionEnded": { + "message": "A Prémium előfizetés véget ért." + }, + "archivePremiumRestart": { + "message": "Az archívumhoz hozzáférés visszaszerzéséhez indítsuk újra a Prémium előfizetést. Ha az újraindítás előtt szerkesztjük egy archivált elem adatait, akkor az visszakerül a széfbe." + }, + "restartPremium": { + "message": "Prémium előfizetés újraindítása" + }, "ppremiumSignUpReports": { "message": "Jelszó higiénia, fiók biztonság és adatszivárgási jelentések a széf biztonsága érdekében." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Adminisztrátori konzol" }, + "admin": { + "message": "Adminisztrátor" + }, + "automaticUserConfirmation": { + "message": "Automatikus felhasználói megerősítés" + }, + "automaticUserConfirmationHint": { + "message": "A függőben lévő felhasználók automatikus megerősítése az eszköz zárolásának feloldásakor." + }, + "autoConfirmOnboardingCallout": { + "message": "Idő megtakarítás az automatikus felhasználói megerősítéssel" + }, + "autoConfirmWarning": { + "message": "Ez hatással lehet a szervezet adatbiztonságára." + }, + "autoConfirmWarningLink": { + "message": "További információ a kockázatokról" + }, + "autoConfirmSetup": { + "message": "Új felhasználók automatikus megerősítése" + }, + "autoConfirmSetupDesc": { + "message": "Az új felhasználók automatikusan megerősítésre kerülnek, amíg ez az eszköz fel van oldva." + }, + "autoConfirmSetupHint": { + "message": "Melyek a lehetséges biztonsági kockázatok?" + }, + "autoConfirmEnabled": { + "message": "Az automatikus megerősítés bekapcsolásra került." + }, + "availableNow": { + "message": "Elérhető most" + }, "accountSecurity": { "message": "Fiókbiztonság" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ez a bejelentkezés veszélyben van és hiányzik egy webhely. Adjunk hozzá egy webhelyet és módosítsuk a jelszót az erősebb biztonság érdekében." }, + "vulnerablePassword": { + "message": "A jelszó sérülékeny." + }, + "changeNow": { + "message": "Módosítás most" + }, "missingWebsite": { "message": "Hiányzó webhely" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Miért látható ez?" + }, + "resizeSideNavigation": { + "message": "Oldalnavigáció átméretezés" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 4d636a5a79d..08d346d57d8 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Pilihan masuk dua-langkah yang dipatenkan seperti YubiKey dan Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Kebersihan kata sandi, kesehatan akun, dan laporan kebocoran data untuk tetap menjaga keamanan brankas Anda." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Konsol Admin" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Keamanan akun" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 2615640df0a..32fe8e9ce54 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Elemento archiviato" }, + "itemWasUnarchived": { + "message": "Elemento rimosso dall'archivio" + }, "itemUnarchived": { "message": "Elemento rimosso dall'archivio" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Gli elementi archiviati sono esclusi dai risultati di ricerca e suggerimenti di autoriempimento. Vuoi davvero archiviare questo elemento?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Per utilizzare Archivio è necessario un abbonamento premium." }, + "itemRestored": { + "message": "L'elemento è stato ripristinato" + }, "edit": { "message": "Modifica" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Sicurezza delle password, integrità dell'account, e rapporti su violazioni di dati per mantenere sicura la tua cassaforte." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Console di amministrazione" }, + "admin": { + "message": "Amministratore" + }, + "automaticUserConfirmation": { + "message": "Conferma automatica degli utenti" + }, + "automaticUserConfirmationHint": { + "message": "Conferma automaticamente gli utenti in sospeso mentre il dispositivo è sbloccato" + }, + "autoConfirmOnboardingCallout": { + "message": "Risparmia tempo con la conferma automatica degli utenti" + }, + "autoConfirmWarning": { + "message": "Potrebbe influenzare la sicurezza dei dati della tua organizzazione. " + }, + "autoConfirmWarningLink": { + "message": "Scopri quali sono i rischi" + }, + "autoConfirmSetup": { + "message": "Conferma automaticamente i nuovi utenti" + }, + "autoConfirmSetupDesc": { + "message": "I nuovi utenti saranno automaticamente confermati mentre questo dispositivo è sbloccato." + }, + "autoConfirmSetupHint": { + "message": "Quali sono i rischi potenziali per la sicurezza?" + }, + "autoConfirmEnabled": { + "message": "Conferma automatica attivata" + }, + "availableNow": { + "message": "Disponibile ora" + }, "accountSecurity": { "message": "Sicurezza dell'account" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Questo login è a rischio e non contiene un sito web. Aggiungi un sito web e cambia la password per maggiore sicurezza." }, + "vulnerablePassword": { + "message": "Password vulnerabile." + }, + "changeNow": { + "message": "Cambiala subito!" + }, "missingWebsite": { "message": "Sito web mancante" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Perché vedo questo avviso?" + }, + "resizeSideNavigation": { + "message": "Ridimensiona la navigazione laterale" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 91c006fccca..0abea0e0236 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "アイテムはアーカイブに送信されました" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "アイテムはアーカイブから解除されました" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "アーカイブされたアイテムはここに表示され、通常の検索結果および自動入力の候補から除外されます。このアイテムをアーカイブしますか?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "アーカイブを使用するにはプレミアムメンバーシップが必要です。" }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "編集" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。" }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "保管庫を安全に保つための、パスワードやアカウントの健全性、データ侵害に関するレポート" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "管理コンソール" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "アカウントのセキュリティ" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "ウェブサイトがありません" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 85c2b04f469..dc104c4a7f3 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "ჩასწორება" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "ანგარიშის უსაფრთხოება" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index c29ecc2d04f..7e0f427ea69 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "ಎಡಿಟ್" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸುರಕ್ಷಿತವಾಗಿರಿಸಲು ಪಾಸ್ವರ್ಡ್ ನೈರ್ಮಲ್ಯ, ಖಾತೆ ಆರೋಗ್ಯ ಮತ್ತು ಡೇಟಾ ಉಲ್ಲಂಘನೆ ವರದಿಗಳು." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index ccf3d96d366..602c54c5f64 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "항목이 보관함으로 이동되었습니다" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "항목 보관 해제됨" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "보관된 항목은 일반 검색 결과와 자동 완성 제안에서 제외됩니다. 이 항목을 보관하시겠습니까?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "편집" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "YubiKey나 Duo와 같은 독점적인 2단계 로그인 옵션" }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "관리자 콘솔" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "계정 보안" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index a1e3e287892..4ba52318c0b 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Keisti" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Patentuotos dviejų žingsnių prisijungimo parinktys, tokios kaip YubiKey ir Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Slaptažodžio higiena, prieigos sveikata ir duomenų nutekinimo ataskaitos, kad tavo saugyklas būtų saugus." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Administratoriaus konsolės" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Paskyros saugumas" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 95e5d2399e6..e75255ab829 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Vienums tika ievietots arhīvā" }, + "itemWasUnarchived": { + "message": "Vienums tika izņemts no arhīva" + }, "itemUnarchived": { "message": "Vienums tika izņemts no arhīva" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Arhivētie vienumi netiek iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos. Vai tiešām ahrivēt šo vienumu?" }, + "archived": { + "message": "Arhivēts" + }, + "unarchiveAndSave": { + "message": "Atcelt arhivēšanu un saglabāt" + }, "upgradeToUseArchive": { "message": "Ir nepieciešama Premium dalība, lai izmantotu arhīvu." }, + "itemRestored": { + "message": "Vienums tika atjaunots" + }, "edit": { "message": "Labot" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo." }, + "premiumSubscriptionEnded": { + "message": "Tavs Premium abonements beidzās" + }, + "archivePremiumRestart": { + "message": "Lai atgūtu piekļuvi savam arhīvam, jāatsāk Premium abonements. Ja labosi arhivēta vienuma informāciju pirms atsākšanas, tas tiks pārvietots atpakaļ Tavā glabātavā." + }, + "restartPremium": { + "message": "Atsākt Premium" + }, "ppremiumSignUpReports": { "message": "Paroļu higiēnas, konta veselības un datu noplūžu pārskati, lai uzturētu glabātavu drošu." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "pārvaldības konsolē," }, + "admin": { + "message": "Pārvaldītājs" + }, + "automaticUserConfirmation": { + "message": "Automātiska lietotāju apstiprināšana" + }, + "automaticUserConfirmationHint": { + "message": "Automātiski apstiprināt ierindotos lietotājus, kamēr šī ierīce ir atslēgta" + }, + "autoConfirmOnboardingCallout": { + "message": "Laika ietaupīšana ar automātisku lietotāju apstiprināšanu" + }, + "autoConfirmWarning": { + "message": "Tas varētu ietekmēt apvienības datu drošību. " + }, + "autoConfirmWarningLink": { + "message": "Uzzināt par riskiem" + }, + "autoConfirmSetup": { + "message": "Automātiski apstiprināt jaunus lietotājus" + }, + "autoConfirmSetupDesc": { + "message": "Jauni lietotāji tiks automātiski apstiprināti, kamēr šī ierīce ir atslēgta." + }, + "autoConfirmSetupHint": { + "message": "Kādi ir iespējamie drošības riski?" + }, + "autoConfirmEnabled": { + "message": "Automātiska apstiprināšana ieslēgta" + }, + "availableNow": { + "message": "Pieejams tagad" + }, "accountSecurity": { "message": "Konta drošība" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Šis pieteikšanās vienums ir pakļauts riskam, un tam nav norādīta tīmekļvietne. Lielākai drošībai jāpievieno tīmekļvietne un jānomaina parole." }, + "vulnerablePassword": { + "message": "Ievainojama parole." + }, + "changeNow": { + "message": "Mainīt tagad" + }, "missingWebsite": { "message": "Nav norādīta tīmekļvietne" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Mainīt sānu pārvietošanās joslas izmēru" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 3257c4c745c..7afe2b75287 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "തിരുത്തുക" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "നിങ്ങളുടെ വാൾട് സൂക്ഷിക്കുന്നതിന്. പാസ്‌വേഡ് ശുചിത്വം, അക്കൗണ്ട് ആരോഗ്യം, ഡാറ്റ ലംഘന റിപ്പോർട്ടുകൾ." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 38487d00f9d..4d355428078 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 9056ce4ad8e..47f9ef4f7a9 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Rediger" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Passordhygiene, kontohelse, og databruddsrapporter som holder hvelvet ditt trygt." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Administrasjonskonsoll" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Kontosikkerhet" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index ea040cb057b..0157fa800f9 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item naar archief verzonden" }, + "itemWasUnarchived": { + "message": "Item uit het archief gehaald" + }, "itemUnarchived": { "message": "Item uit het archief gehaald" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Gearchiveerde items worden uitgesloten van algemene zoekresultaten en automatische invulsuggesties. Weet je zeker dat je dit item wilt archiveren?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Je hebt een Premium-abonnement nodig om te kunnen archiveren." }, + "itemRestored": { + "message": "Item is hersteld" + }, "edit": { "message": "Bewerken" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Wachtwoordhygiëne, gezondheid van je account en datalekken om je kluis veilig te houden." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Beheerconsole" }, + "admin": { + "message": "Beheerder" + }, + "automaticUserConfirmation": { + "message": "Automatische gebruikersbevestiging" + }, + "automaticUserConfirmationHint": { + "message": "Automatisch gebruikers in behandeling bevestigen wanneer dit apparaat is ontgrendeld" + }, + "autoConfirmOnboardingCallout": { + "message": "Bespaar tijd met automatische gebruikersbevestiging" + }, + "autoConfirmWarning": { + "message": "Dit kan van invloed zijn op de gegevensbeveiliging van je organisatie. " + }, + "autoConfirmWarningLink": { + "message": "Meer informatie over de risico's" + }, + "autoConfirmSetup": { + "message": "Automatisch nieuwe gebruikers bevestigen" + }, + "autoConfirmSetupDesc": { + "message": "Nieuwe gebruikers worden automatisch bevestigd wanneer dit apparaat is ontgrendeld." + }, + "autoConfirmSetupHint": { + "message": "Wat zijn de mogelijke veiligheidsrisico's?" + }, + "autoConfirmEnabled": { + "message": "Automatische bevestigen ingeschakeld" + }, + "availableNow": { + "message": "Nu beschikbaar" + }, "accountSecurity": { "message": "Accountbeveiliging" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Deze login is in gevaar en mist een website. Voeg een website toe en verander het wachtwoord voor een sterkere beveiliging." }, + "vulnerablePassword": { + "message": "Kwetsbaar wachtwoord." + }, + "changeNow": { + "message": "Nu wijzigen" + }, "missingWebsite": { "message": "Ontbrekende website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Waarom zie ik dit?" + }, + "resizeSideNavigation": { + "message": "Formaat zijnavigatie wijzigen" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 67e7a2a2a00..705fa9565f1 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Element został przeniesiony do archiwum" }, + "itemWasUnarchived": { + "message": "Element został usunięty z archiwum" + }, "itemUnarchived": { "message": "Element został usunięty z archiwum" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Zarchiwizowane elementy są wykluczone z wyników wyszukiwania i sugestii autouzupełniania. Czy na pewno chcesz archiwizować element?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Element został przywrócony" + }, "edit": { "message": "Edytuj" }, @@ -1047,10 +1059,10 @@ "message": "Element został zapisany" }, "savedWebsite": { - "message": "Zapisana witryna" + "message": "Zapisana strona internetowa" }, "savedWebsites": { - "message": "Zapisane witryny ($COUNT$)", + "message": "Zapisane strony internetowe ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -1429,7 +1441,7 @@ "message": "Confirm your identity to continue" }, "enterYourMasterPassword": { - "message": "Enter your master password" + "message": "Wpisz hasło główne" }, "updateSettings": { "message": "Zaktualizuj ustawienia" @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Specjalne opcje logowania dwustopniowego, takie jak YubiKey i Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Raporty bezpieczeństwa haseł, konta i wycieków danych, aby Twoje dane były bezpieczne." }, @@ -1771,13 +1792,13 @@ "message": "W jaki sposób Bitwarden chroni Twoje dane przed phishingiem?" }, "currentWebsite": { - "message": "Aktualna witryna" + "message": "Obecna strona internetowa" }, "autofillAndAddWebsite": { - "message": "Wypełnij automatycznie i dodaj tę witrynę" + "message": "Uzupełnij i dodaj stronę internetową" }, "autofillWithoutAdding": { - "message": "Automatyczne uzupełnianie bez dodawania" + "message": "Uzupełnij bez dodawania" }, "doNotAutofill": { "message": "Nie wypełniaj automatycznie" @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Konsola administratora" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Bezpieczeństwo konta" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Dane logowania są zagrożone i nie zawierają strony internetowej. Dodaj stronę internetową i zmień hasło." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Zmień teraz" + }, "missingWebsite": { "message": "Brak strony internetowej" }, @@ -5894,7 +5954,7 @@ "message": "Sejf załadowany" }, "settingDisabledByPolicy": { - "message": "To ustawienie jest wyłączone zgodnie z zasadami polityki Twojej organizacji.", + "message": "Ustawienie jest wyłączone przez Twoją organizację.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." }, "zipPostalCodeLabel": { @@ -5907,10 +5967,10 @@ "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, "continueWithLogIn": { - "message": "Continue with log in" + "message": "Kontynuuj logowanie" }, "doNotContinue": { - "message": "Do not continue" + "message": "Nie kontynuuj" }, "domain": { "message": "Domena" @@ -5943,7 +6003,7 @@ "message": "To continue with log in, verify the organization and domain." }, "sessionTimeoutSettingsAction": { - "message": "Akcja przekroczenia limitu czasu" + "message": "Sposób blokady" }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." @@ -6002,7 +6062,7 @@ "message": "Contact your admin to regain access." }, "leaveConfirmationDialogConfirmButton": { - "message": "Leave $ORGANIZATION$", + "message": "Opuść organizację $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6011,10 +6071,10 @@ } }, "howToManageMyVault": { - "message": "How do I manage my vault?" + "message": "Jak zarządzać sejfami?" }, "transferItemsToOrganizationTitle": { - "message": "Transfer items to $ORGANIZATION$", + "message": "Przenieś elementy do organizacji $ORGANIZATION$", "placeholders": { "organization": { "content": "$1", @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Zmień rozmiar nawigacji bocznej" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index c7ecfe3f81d..5027fb35af2 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -203,13 +203,13 @@ "message": "Preenchimento automático" }, "autoFillLogin": { - "message": "Preencher credencial automaticamente" + "message": "Preencher credencial" }, "autoFillCard": { - "message": "Preencher cartão automaticamente" + "message": "Preencher cartão" }, "autoFillIdentity": { - "message": "Preencher identidade automaticamente" + "message": "Preencher identidade" }, "fillVerificationCode": { "message": "Preencher código de verificação" @@ -249,7 +249,7 @@ "message": "Conecte-se ao seu cofre" }, "autoFillInfo": { - "message": "Não há credenciais disponíveis para preencher automaticamente na aba atual do navegador." + "message": "Não há credenciais disponíveis para preencher na aba atual do navegador." }, "addLogin": { "message": "Adicionar uma credencial" @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "O item foi enviado para o arquivo" }, + "itemWasUnarchived": { + "message": "O item foi desarquivado" + }, "itemUnarchived": { "message": "O item foi desarquivado" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Itens arquivados são excluídos dos resultados gerais de busca e das sugestões de preenchimento automático. Tem certeza de que deseja arquivar este item?" }, + "archived": { + "message": "Arquivados" + }, + "unarchiveAndSave": { + "message": "Desarquivar e salvar" + }, "upgradeToUseArchive": { "message": "Um plano Premium é necessário para usar o arquivamento." }, + "itemRestored": { + "message": "O item foi restaurado" + }, "edit": { "message": "Editar" }, @@ -895,7 +907,7 @@ } }, "autofillError": { - "message": "Não é possível preencher automaticamente o item selecionado nesta página. Em vez disso, copie e cole a informação." + "message": "Não é possível preencher o item selecionado nesta página. Em vez disso, copie e cole a informação." }, "totpCaptureError": { "message": "Não é possível ler o código QR da página atual" @@ -1120,7 +1132,7 @@ "message": "Listar as identidades na página da aba para facilitar o preenchimento automático." }, "clickToAutofillOnVault": { - "message": "Clique em itens na tela do Cofre para preencher automaticamente" + "message": "Clique em itens na tela do Cofre para preencher" }, "clickToAutofill": { "message": "Clicar itens na sugestão para preenchê-lo" @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opções de autenticação em duas etapas proprietárias como YubiKey e Duo." }, + "premiumSubscriptionEnded": { + "message": "Sua assinatura Premium terminou" + }, + "archivePremiumRestart": { + "message": "Para recuperar o seu acesso ao seu arquivo, retoma sua assinatura Premium. Se editar detalhes de um item arquivado antes de retomar, ele será movido de volta para o seu cofre." + }, + "restartPremium": { + "message": "Retomar Premium" + }, "ppremiumSignUpReports": { "message": "Relatórios de higiene de senha, saúde da conta, e vazamentos de dados para manter o seu cofre seguro." }, @@ -1585,7 +1606,7 @@ "message": "Copiar TOTP automaticamente" }, "disableAutoTotpCopyDesc": { - "message": "Se uma credencial tiver uma chave de autenticador, copie o código de verificação TOTP quando for preenchê-la automaticamente." + "message": "Se uma credencial tiver uma chave de autenticador, copie o código de verificação TOTP quando for preenchê-la." }, "enableAutoBiometricsPrompt": { "message": "Pedir biometria ao abrir" @@ -1774,13 +1795,13 @@ "message": "Site atual" }, "autofillAndAddWebsite": { - "message": "Preencher automaticamente e adicionar este site" + "message": "Preencher e adicionar este site" }, "autofillWithoutAdding": { - "message": "Preencher automaticamente sem adicionar" + "message": "Preencher sem adicionar" }, "doNotAutofill": { - "message": "Não preencher automaticamente" + "message": "Não preencher" }, "showInlineMenuIdentitiesLabel": { "message": "Exibir identidades como sugestões" @@ -1813,10 +1834,10 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Preencher automaticamente ao carregar a página" + "message": "Preenchimento no carregamento da página" }, "enableAutoFillOnPageLoad": { - "message": "Preencher automaticamente ao carregar a página" + "message": "Preencher ao carregar a página" }, "enableAutoFillOnPageLoadDesc": { "message": "Se um formulário de credencial for detectado, preencha automaticamente quando a página carregar." @@ -1840,10 +1861,10 @@ "message": "Usar configuração padrão" }, "autoFillOnPageLoadYes": { - "message": "Preencher automaticamente ao carregar a página" + "message": "Preencher ao carregar a página" }, "autoFillOnPageLoadNo": { - "message": "Não preencher automaticamente ao carregar a página" + "message": "Não preencher ao carregar a página" }, "commandOpenPopup": { "message": "Abrir pop-up do cofre" @@ -1852,13 +1873,13 @@ "message": "Abrir cofre na barra lateral" }, "commandAutofillLoginDesc": { - "message": "Preencher automaticamente a última credencial utilizada para o site atual" + "message": "Preencher com a última credencial utilizada para o site atual" }, "commandAutofillCardDesc": { - "message": "Preencher automaticamente o último cartão utilizado para o site atual" + "message": "Preencher com o último cartão utilizado para o site atual" }, "commandAutofillIdentityDesc": { - "message": "Preencher automaticamente a última identidade usada para o site atual" + "message": "Preencher com a última identidade usada para o site atual" }, "commandGeneratePasswordDesc": { "message": "Gere e copie uma nova senha aleatória para a área de transferência" @@ -2468,7 +2489,7 @@ "message": "Confirmação da ação do limite de tempo" }, "autoFillAndSave": { - "message": "Preencher automaticamente e salvar" + "message": "Preencher e salvar" }, "fillAndSave": { "message": "Preencher e salvar" @@ -2486,7 +2507,7 @@ "message": "Você ainda deseja preencher esta credencial?" }, "autofillIframeWarning": { - "message": "O formulário está hospedado em um domínio diferente do URI da sua credencial salva. Escolha OK para preencher automaticamente mesmo assim, ou Cancelar para parar." + "message": "O formulário está hospedado em um domínio diferente do URI da sua credencial salva. Escolha OK para preencher mesmo assim, ou Cancelar para parar." }, "autofillIframeWarningTip": { "message": "Para evitar este aviso no futuro, salve este URI, $HOSTNAME$, no seu item de credencial no Bitwarden para este site.", @@ -2812,7 +2833,7 @@ "message": "Altere senhas em risco mais rápido" }, "changeAtRiskPasswordsFasterDesc": { - "message": "Atualize suas configurações para poder preencher senhas automaticamente ou gerá-las automaticamente" + "message": "Atualize suas configurações para poder preencher ou gerar novas senhas" }, "reviewAtRiskLogins": { "message": "Revisar credenciais em risco" @@ -4121,7 +4142,7 @@ "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "cannotAutofill": { - "message": "Não é possível preencher automaticamente" + "message": "Não é possível preencher" }, "cannotAutofillExactMatch": { "message": "A correspondência padrão está configurada como 'Correspondência exata'. O site atual não corresponde exatamente aos detalhes salvos de credencial para este item." @@ -4677,7 +4698,7 @@ "message": "Itens sugeridos" }, "autofillSuggestionsTip": { - "message": "Salve um item de credencial para este site para preencher automaticamente" + "message": "Salve um item de credencial para este site para preencher" }, "yourVaultIsEmpty": { "message": "Seu cofre está vazio" @@ -4753,7 +4774,7 @@ } }, "autofillTitle": { - "message": "Preencher automaticamente - $ITEMNAME$", + "message": "Preencher - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4763,7 +4784,7 @@ } }, "autofillTitleWithField": { - "message": "Preencher automaticamente - $ITEMNAME$ - $FIELD$", + "message": "Preencher - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Painel de administração" }, + "admin": { + "message": "Administrador" + }, + "automaticUserConfirmation": { + "message": "Confirmação automática de usuários" + }, + "automaticUserConfirmationHint": { + "message": "Confirme automaticamente usuários pendentes quando este dispositivo for desbloqueado" + }, + "autoConfirmOnboardingCallout": { + "message": "Economize tempo com a confirmação automática de usuários" + }, + "autoConfirmWarning": { + "message": "Isso pode afetar a segurança dos dados da sua organização. " + }, + "autoConfirmWarningLink": { + "message": "Saiba mais sobre os riscos" + }, + "autoConfirmSetup": { + "message": "Confirmar automaticamente usuários novos" + }, + "autoConfirmSetupDesc": { + "message": "Usuários novos serão confirmados automaticamente quando este dispositivo for desbloqueado." + }, + "autoConfirmSetupHint": { + "message": "Quais são os possíveis problemas de segurança?" + }, + "autoConfirmEnabled": { + "message": "Ativou a confirmação automática" + }, + "availableNow": { + "message": "Disponível agora" + }, "accountSecurity": { "message": "Segurança da conta" }, @@ -4853,7 +4907,7 @@ } }, "new": { - "message": "Novo" + "message": "Criar" }, "removeItem": { "message": "Remover $NAME$", @@ -5081,7 +5135,7 @@ } }, "autoFillOnPageLoad": { - "message": "Preencher automaticamente ao carregar a página?" + "message": "Preencher ao carregar a página?" }, "cardExpiredTitle": { "message": "Cartão vencido" @@ -5165,7 +5219,7 @@ "message": "Use campos ocultos para dados confidenciais como uma senha" }, "checkBoxHelpText": { - "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um lembrar e-mail" + "message": "Use caixas de seleção se gostaria de preencher a caixa de seleção de um formulário, como um lembrar e-mail" }, "linkedHelpText": { "message": "Use um campo vinculado quando estiver enfrentando problemas com o preenchimento automático com um site específico." @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e está sem um site. Adicione um site e altere a senha para segurança melhor." }, + "vulnerablePassword": { + "message": "Senha vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site ausente" }, @@ -5748,7 +5808,7 @@ } }, "hasItemsVaultNudgeBodyOne": { - "message": "Preenche automaticamente itens para a página atual" + "message": "Preenche itens para a página atual" }, "hasItemsVaultNudgeBodyTwo": { "message": "Favorite itens para acesso rápido" @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Por que estou vendo isso?" + }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index d766e8f95fb..7f97ab08623 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "O item foi movido para o arquivo" }, + "itemWasUnarchived": { + "message": "O item foi desarquivado" + }, "itemUnarchived": { "message": "O item foi desarquivado" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Os itens arquivados são excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático. Tem a certeza de que pretende arquivar este item?" }, + "archived": { + "message": "Arquivado" + }, + "unarchiveAndSave": { + "message": "Desarquivar e guardar" + }, "upgradeToUseArchive": { "message": "É necessária uma subscrição Premium para utilizar o Arquivo." }, + "itemRestored": { + "message": "O item foi restaurado" + }, "edit": { "message": "Editar" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo." }, + "premiumSubscriptionEnded": { + "message": "A sua subscrição Premium terminou" + }, + "archivePremiumRestart": { + "message": "Para recuperar o acesso ao seu arquivo, reinicie a sua subscrição Premium. Se editar os detalhes de um item arquivado antes de reiniciar, ele será movido de volta para o seu cofre." + }, + "restartPremium": { + "message": "Reiniciar Premium" + }, "ppremiumSignUpReports": { "message": "Higiene de palavras-passe, saúde da conta e relatórios de violação de dados para manter o seu cofre seguro." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Consola de administração" }, + "admin": { + "message": "Administrador" + }, + "automaticUserConfirmation": { + "message": "Confirmação automática do utilizador" + }, + "automaticUserConfirmationHint": { + "message": "Confirmar automaticamente os utilizadores pendentes enquanto este dispositivo estiver desbloqueado" + }, + "autoConfirmOnboardingCallout": { + "message": "Poupe tempo com a confirmação automática de utilizadores" + }, + "autoConfirmWarning": { + "message": "Isto pode afetar a segurança dos dados da sua organização. " + }, + "autoConfirmWarningLink": { + "message": "Saiba mais sobre os riscos" + }, + "autoConfirmSetup": { + "message": "Confirmar automaticamente novos utilizadores" + }, + "autoConfirmSetupDesc": { + "message": "Os novos utilizadores serão automaticamente confirmados enquanto este dispositivo estiver desbloqueado." + }, + "autoConfirmSetupHint": { + "message": "Quais são os riscos potenciais de segurança?" + }, + "autoConfirmEnabled": { + "message": "Confirmação automática ativada" + }, + "availableNow": { + "message": "Já disponível" + }, "accountSecurity": { "message": "Segurança da conta" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e não tem um site. Adicione um site e altere a palavra-passe para uma segurança mais forte." }, + "vulnerablePassword": { + "message": "Palavra-passe vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site em falta" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Porque é que estou a ver isto?" + }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index f0f71168074..139b94d1a55 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Editare" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Opțiuni brevetate de conectare cu doi factori, cum ar fi YubiKey și Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Rapoarte privind igiena parolelor, sănătatea contului și breșele de date pentru a vă păstra seiful în siguranță." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 3185edea5d5..0ab286c819d 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Элемент был отправлен в архив" }, + "itemWasUnarchived": { + "message": "Элемент был разархивирован" + }, "itemUnarchived": { "message": "Элемент был разархивирован" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Архивированные элементы исключены из общих результатов поиска и предложений автозаполнения. Вы уверены, что хотите архивировать этот элемент?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Для использования архива требуется премиум-статус." }, + "itemRestored": { + "message": "Элемент восстановлен" + }, "edit": { "message": "Изменить" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Гигиена паролей, здоровье аккаунта и отчеты об утечках данных для обеспечения безопасности вашего хранилища." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "консоли администратора" }, + "admin": { + "message": "Администратор" + }, + "automaticUserConfirmation": { + "message": "Автоматическое подтверждение пользователя" + }, + "automaticUserConfirmationHint": { + "message": "Автоматически подтверждать ожидающих пользователей пока это устройство разблокировано" + }, + "autoConfirmOnboardingCallout": { + "message": "Экономьте время благодаря автоматическому подтверждению пользователей" + }, + "autoConfirmWarning": { + "message": "Это может повлиять на безопасность данных вашей организации. " + }, + "autoConfirmWarningLink": { + "message": "Узнайте о рисках" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Безопасность аккаунта" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Этот логин подвержен риску и у него отсутствует веб-сайт. Добавьте веб-сайт и смените пароль для большей безопасности." }, + "vulnerablePassword": { + "message": "Уязвимый пароль." + }, + "changeNow": { + "message": "Изменить сейчас" + }, "missingWebsite": { "message": "Отсутствует сайт" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Почему я это вижу?" + }, + "resizeSideNavigation": { + "message": "Изменить размер боковой навигации" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 7ef837967c9..82fed7f5fd4 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "සංස්කරණය" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "ඔබගේ සුරක්ෂිතාගාරය ආරක්ෂිතව තබා ගැනීම සඳහා මුරපදය සනීපාරක්ෂාව, ගිණුම් සෞඛ්යය සහ දත්ත උල්ලං ach නය වාර්තා කරයි." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index ffda610b8f0..f5619ef017d 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Položka bola archivovaná" }, + "itemWasUnarchived": { + "message": "Položka bola odobraná z archívu" + }, "itemUnarchived": { "message": "Položka bola odobraná z archívu" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať túto položku?" }, + "archived": { + "message": "Archivované" + }, + "unarchiveAndSave": { + "message": "Zrušiť archiváciu a uložiť" + }, "upgradeToUseArchive": { "message": "Na použitie archívu je potrebné prémiové členstvo." }, + "itemRestored": { + "message": "Položka bola obnovená" + }, "edit": { "message": "Upraviť" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo." }, + "premiumSubscriptionEnded": { + "message": "Vaše predplatné Prémium skončilo" + }, + "archivePremiumRestart": { + "message": "Ak chcete obnoviť prístup k svojmu archívu, reštartujte predplatné Prémium. Ak pred reštartom upravíte podrobnosti archivovanej položky, bude presunutá späť do trezoru." + }, + "restartPremium": { + "message": "Reštartovať Prémium" + }, "ppremiumSignUpReports": { "message": "Správy o sile hesla, zabezpečení účtov a únikoch dát ktoré vám pomôžu udržať vaše kontá v bezpečí." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Správcovská konzola" }, + "admin": { + "message": "Správca" + }, + "automaticUserConfirmation": { + "message": "Automatické potvrdenie používateľa" + }, + "automaticUserConfirmationHint": { + "message": "Automaticky potvrdzovať čakajúcich používateľov, keď je toto zariadenie odomknuté" + }, + "autoConfirmOnboardingCallout": { + "message": "Šetrite čas automatickým potvrdzovaním používateľa" + }, + "autoConfirmWarning": { + "message": "Môže mať vplyv na bezpečnosť údajov vašej organizácie. " + }, + "autoConfirmWarningLink": { + "message": "Dozvedieť sa viac o rizikách" + }, + "autoConfirmSetup": { + "message": "Automaticky potvrdzovať nových používateľov" + }, + "autoConfirmSetupDesc": { + "message": "Noví používatelia budú automaticky potvrdení, keď je toto zariadenie odomknuté." + }, + "autoConfirmSetupHint": { + "message": "Aké sú potenciálne bezpečnostné riziká?" + }, + "autoConfirmEnabled": { + "message": "Zapnuté automatické potvrdzovanie" + }, + "availableNow": { + "message": "Teraz dostupné" + }, "accountSecurity": { "message": "Zabezpečenie účtu" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Toto prihlásenie je v ohrození a chýba mu webová stránka. Pridajte webovú stránku a zmeňte heslo na silnejšie zabezpečenie." }, + "vulnerablePassword": { + "message": "Zraniteľné heslo." + }, + "changeNow": { + "message": "Zmeniť teraz" + }, "missingWebsite": { "message": "Chýbajúca webová stránka" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Prečo to vidím?" + }, + "resizeSideNavigation": { + "message": "Zmeniť veľkosť bočnej navigácie" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2806020d5f0..a0ccbdeadbc 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Uredi" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Higiena gesel, zdravje računa in poročila o kraji podatkov, ki vam pomagajo ohraniti varnost vašega trezorja." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 4a23b7e7141..f5e85127639 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Ставка је послата у архиву" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Ставка враћена из архиве" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Архивиране ставке су искључене из општих резултата претраге и предлога за ауто попуњавање. Јесте ли сигурни да желите да архивирате ову ставку?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Премијум чланство је неопходно за употребу Архиве." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Уреди" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Извештаји о хигијени лозинки, здравственом стању налога и кршењу података да бисте заштитили сеф." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Администраторска конзола" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Безбедност налога" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ова пријава је ризична и недостаје веб локација. Додајте веб страницу и промените лозинку за јачу сигурност." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Недостаје веб страница" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Зашто видите ово?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d04202f5c0b..2aef3f0d6d8 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Objektet skickades till arkivet" }, + "itemWasUnarchived": { + "message": "Objektet har avarkiverats" + }, "itemUnarchived": { "message": "Objektet har avarkiverats" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Arkiverade objekt är exkluderade från allmänna sökresultat och förslag för autofyll. Är du säker på att du vill arkivera detta objekt?" }, + "archived": { + "message": "Arkiverade" + }, + "unarchiveAndSave": { + "message": "Avarkivera och spara" + }, "upgradeToUseArchive": { "message": "Ett premium-medlemskap krävs för att använda Arkiv." }, + "itemRestored": { + "message": "Objektet har återställts" + }, "edit": { "message": "Redigera" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Premium-alternativ för tvåstegsverifiering, såsom YubiKey och Duo." }, + "premiumSubscriptionEnded": { + "message": "Ditt Premium-abonnemang avslutades" + }, + "archivePremiumRestart": { + "message": "För att återfå åtkomst till ditt arkiv, starta om Premium-abonnemanget. Om du redigerar detaljer för ett arkiverat objekt innan du startar om kommer det att flyttas tillbaka till ditt valv." + }, + "restartPremium": { + "message": "Starta om Premium" + }, "ppremiumSignUpReports": { "message": "Lösenordshygien, kontohälsa och dataintrångsrapporter för att hålla ditt valv säkert." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Adminkonsol" }, + "admin": { + "message": "Administratör" + }, + "automaticUserConfirmation": { + "message": "Automatisk bekräftelse av användare" + }, + "automaticUserConfirmationHint": { + "message": "Bekräfta automatiskt väntande användare medan enheten är olåst" + }, + "autoConfirmOnboardingCallout": { + "message": "Spara tid med automatisk användarbekräftelse" + }, + "autoConfirmWarning": { + "message": "Detta kan påverka din organisations datasäkerhet. " + }, + "autoConfirmWarningLink": { + "message": "Läs mer om riskerna" + }, + "autoConfirmSetup": { + "message": "Bekräfta nya användare automatiskt" + }, + "autoConfirmSetupDesc": { + "message": "Nya användare kommer automatiskt att bekräftas när denna enhet är upplåst." + }, + "autoConfirmSetupHint": { + "message": "Vilka är de potentiella säkerhetsriskerna?" + }, + "autoConfirmEnabled": { + "message": "Aktiverade automatisk bekräftelse" + }, + "availableNow": { + "message": "Tillgänglig nu" + }, "accountSecurity": { "message": "Kontosäkerhet" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Denna inloggning är utsatt för risk och saknar en webbplats. Lägg till en webbplats och ändra lösenordet för ökad säkerhet." }, + "vulnerablePassword": { + "message": "Sårbart lösenord." + }, + "changeNow": { + "message": "Ändra nu" + }, "missingWebsite": { "message": "Saknar webbplats" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Varför ser jag det här?" + }, + "resizeSideNavigation": { + "message": "Ändra storlek på sidnavigering" } } diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 1e25b5157ef..c329028526a 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "ஆவணம் காப்பகத்திற்கு அனுப்பப்பட்டது" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "காப்பகம் மீட்டெடுக்கப்பட்டது" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "காப்பகப்படுத்தப்பட்ட உருப்படிகள் பொதுவான தேடல் முடிவுகள் மற்றும் தானியங்குநிரப்பு பரிந்துரைகளிலிருந்து விலக்கப்பட்டுள்ளன. இந்த உருப்படியை காப்பகப்படுத்த விரும்புகிறீர்களா?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "காப்பகத்தைப் பயன்படுத்த பிரீமியம் உறுப்பினர் தேவை." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "திருத்து" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "YubiKey மற்றும் Duo போன்ற பிரத்யேக டூ-ஸ்டெப் உள்நுழைவு விருப்பங்கள்." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "உங்கள் வால்ட்டைப் பாதுகாப்பாக வைத்திருக்க கடவுச்சொல் சுகாதாரம், கணக்கின் ஆரோக்கியம் மற்றும் டேட்டா மீறல் அறிக்கைகள்." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "நிர்வாகக் கன்சோல்" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "கணக்கு பாதுகாப்பு" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "இந்த உள்நுழைவு ஆபத்தில் உள்ளது, மேலும் அதில் ஒரு வலைத்தளமும் இல்லை. வலுவான பாதுகாப்பிற்காக ஒரு வலைத்தளத்தைச் சேர்த்து கடவுச்சொல்லை மாற்றவும்." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "காணாமல் போன வலைத்தளம்" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 04386b72930..db750969f43 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "A premium membership is required to use Archive." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Edit" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Password hygiene, account health, and data breach reports to keep your vault safe." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Admin Console" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Account security" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Why am I seeing this?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 49bda58b558..49ed1cebd89 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "ย้ายรายการไปที่จัดเก็บถาวรแล้ว" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "เลิกจัดเก็บถาวรรายการแล้ว" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "รายการที่จัดเก็บถาวรจะไม่ถูกรวมในผลการค้นหาทั่วไปและคำแนะนำการป้อนอัตโนมัติ ยืนยันที่จะจัดเก็บรายการนี้ถาวรหรือไม่" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "ต้องเป็นสมาชิกพรีเมียมจึงจะใช้งานฟีเจอร์จัดเก็บถาวรได้" }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "แก้ไข" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "ตัวเลือกการเข้าสู่ระบบ 2 ขั้นตอนแบบพิเศษ เช่น YubiKey และ Duo" }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "รายงานความปลอดภัยของรหัสผ่าน สุขภาพบัญชี และข้อมูลรั่วไหล เพื่อรักษาตู้นิรภัยให้ปลอดภัย" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "คอนโซลผู้ดูแลระบบ" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "ความปลอดภัยของบัญชี" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "ข้อมูลเข้าสู่ระบบนี้มีความเสี่ยงและไม่มีเว็บไซต์ เพิ่มเว็บไซต์และเปลี่ยนรหัสผ่านเพื่อความปลอดภัยที่รัดกุมยิ่งขึ้น" }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "ไม่มีเว็บไซต์" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "ทำไมฉันจึงเห็นสิ่งนี้" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index e6a787716a4..344b8e03835 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Kayıt arşive gönderildi" }, + "itemWasUnarchived": { + "message": "Kayıt arşivden çıkarıldı" + }, "itemUnarchived": { "message": "Kayıt arşivden çıkarıldı" }, @@ -580,11 +583,20 @@ "message": "Kaydı arşivle" }, "archiveItemConfirmDesc": { - "message": "Arşivlenmiş kayıtlar genel arama sonuçları ve otomatik doldurma önerilerinden hariç tutulur. Bu kaydı arşivlemek istediğine emin misin?" + "message": "Arşivlenmiş kayıtlar genel arama sonuçları ve otomatik doldurma önerilerinden hariç tutulur. Bu kaydı arşivlemek istediğinizden emin misiniz?" + }, + "archived": { + "message": "Arşivlendi" + }, + "unarchiveAndSave": { + "message": "Arşivden çıkar ve kaydet" }, "upgradeToUseArchive": { "message": "Arşivi kullanmak için premium üyelik gereklidir." }, + "itemRestored": { + "message": "Kayıt geri yüklendi" + }, "edit": { "message": "Düzenle" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri." }, + "premiumSubscriptionEnded": { + "message": "Premium aboneliğiniz sona erdi" + }, + "archivePremiumRestart": { + "message": "Arşivinize yeniden erişebilmek için Premium aboneliğinizi yeniden başlatın. Yeniden başlatmadan önce arşivlenmiş bir kaydın ayrıntılarını düzenlerseniz kayıt tekrar kasanıza taşınır." + }, + "restartPremium": { + "message": "Premium’u yeniden başlat" + }, "ppremiumSignUpReports": { "message": "Kasanızı güvende tutmak için parola hijyeni, hesap sağlığı ve veri ihlali raporları." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Yönetici Konsolu" }, + "admin": { + "message": "Yönetici" + }, + "automaticUserConfirmation": { + "message": "Otomatik kullanıcı onayı" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Yeni kullanıcıları otomatik onayla" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Hesap güvenliği" }, @@ -5216,7 +5270,7 @@ "message": "Web sitesi URI'sini yeniden sıralayın. Kayıtı yukarı veya aşağı taşımak için ok tuşunu kullanın." }, "reorderFieldUp": { - "message": "$LABEL$ yukarı taşındı, konum: $LENGTH$'in $INDEX$'i", + "message": "$LABEL$ yukarı taşındı. Konum: $LENGTH$/$INDEX$", "placeholders": { "label": { "content": "$1", @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu hesap risk altında ve web sitesi eksik. Bir web sitesi ekleyin ve güvenliğinizi artırmak için parolayı değiştirin." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Şimdi değiştir" + }, "missingWebsite": { "message": "Web sitesi eksik" }, @@ -5739,7 +5799,7 @@ "message": "Oltalama tespiti hakkında daha fazla bilgi edinin" }, "protectedBy": { - "message": "$PRODUCT$ tarafından korunuyor", + "message": "$PRODUCT$ ile korunuyor", "placeholders": { "product": { "content": "$1", @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Bunu neden görüyorum?" + }, + "resizeSideNavigation": { + "message": "Kenar menüsünü yeniden boyutlandır" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index d777dee1472..01de2bbadf6 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Запис архівовано" }, + "itemWasUnarchived": { + "message": "Запис розархівовано" + }, "itemUnarchived": { "message": "Запис розархівовано" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Архівовані записи виключаються з результатів звичайного пошуку та пропозицій автозаповнення. Ви дійсно хочете архівувати цей запис?" }, + "archived": { + "message": "Архівовано" + }, + "unarchiveAndSave": { + "message": "Розархівувати й зберегти" + }, "upgradeToUseArchive": { "message": "Для використання архіву необхідна передплата Premium." }, + "itemRestored": { + "message": "Запис відновлено" + }, "edit": { "message": "Змінити" }, @@ -1323,19 +1335,19 @@ "message": "Експортувати з" }, "exportVerb": { - "message": "Export", + "message": "Експортувати", "description": "The verb form of the word Export" }, "exportNoun": { - "message": "Export", + "message": "Експорт", "description": "The noun form of the word Export" }, "importNoun": { - "message": "Import", + "message": "Імпорт", "description": "The noun form of the word Import" }, "importVerb": { - "message": "Import", + "message": "Імпортувати", "description": "The verb form of the word Import" }, "fileFormat": { @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo." }, + "premiumSubscriptionEnded": { + "message": "Ваша передплата Premium завершилась" + }, + "archivePremiumRestart": { + "message": "Щоб відновити доступ до архіву, поновіть передплату Premium. Якщо ви редагуєте архівований запис перед поновленням, його буде повернуто назад у ваше сховище." + }, + "restartPremium": { + "message": "Поновити Premium" + }, "ppremiumSignUpReports": { "message": "Гігієна паролів, здоров'я облікового запису, а також звіти про вразливості даних, щоб зберігати ваше сховище в безпеці." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "консолі адміністратора," }, + "admin": { + "message": "Адміністратор" + }, + "automaticUserConfirmation": { + "message": "Автоматичне підтвердження користувачів" + }, + "automaticUserConfirmationHint": { + "message": "Автоматично підтверджувати користувачів, які перебувають у черзі, поки цей пристрій розблокований" + }, + "autoConfirmOnboardingCallout": { + "message": "Заощаджуйте час завдяки автоматичному підтвердженню користувачів" + }, + "autoConfirmWarning": { + "message": "Це може вплинути на безпеку даних вашої організації. " + }, + "autoConfirmWarningLink": { + "message": "Дізнатися про ризики" + }, + "autoConfirmSetup": { + "message": "Автоматично підтверджувати нових користувачів" + }, + "autoConfirmSetupDesc": { + "message": "Нові користувачі будуть автоматично підтверджені, якщо пристрій розблоковано." + }, + "autoConfirmSetupHint": { + "message": "Які потенційні ризики безпеки?" + }, + "autoConfirmEnabled": { + "message": "Автоматичне підтвердження увімкнено" + }, + "availableNow": { + "message": "Доступно зараз" + }, "accountSecurity": { "message": "Безпека облікового запису" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Цей запис ризикований, і не має адреси вебсайту. Додайте адресу вебсайту і змініть пароль для вдосконалення безпеки." }, + "vulnerablePassword": { + "message": "Вразливий пароль." + }, + "changeNow": { + "message": "Змінити зараз" + }, "missingWebsite": { "message": "Немає вебсайту" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Чому я це бачу?" + }, + "resizeSideNavigation": { + "message": "Змінити розмір бічної панелі" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index cd8ad7cbc91..4ea7ebf885f 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "Mục đã được chuyển vào kho lưu trữ" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Mục đã được bỏ lưu trữ" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "Các mục đã lưu trữ sẽ bị loại khỏi kết quả tìm kiếm chung và gợi ý tự động điền. Bạn có chắc chắn muốn lưu trữ mục này không?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "Cần là thành viên cao cấp để sử dụng tính năng Lưu trữ." }, + "itemRestored": { + "message": "Item has been restored" + }, "edit": { "message": "Sửa" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "Các tùy chọn xác minh hai bước như YubiKey và Duo." }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rỉ dữ liệu để bảo vệ kho dữ liệu của bạn." }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "Bảng điều khiển dành cho quản trị viên" }, + "admin": { + "message": "Admin" + }, + "automaticUserConfirmation": { + "message": "Automatic user confirmation" + }, + "automaticUserConfirmationHint": { + "message": "Automatically confirm pending users while this device is unlocked" + }, + "autoConfirmOnboardingCallout": { + "message": "Save time with automatic user confirmation" + }, + "autoConfirmWarning": { + "message": "This could impact your organization’s data security. " + }, + "autoConfirmWarningLink": { + "message": "Learn about the risks" + }, + "autoConfirmSetup": { + "message": "Automatically confirm new users" + }, + "autoConfirmSetupDesc": { + "message": "New users will be automatically confirmed while this device is unlocked." + }, + "autoConfirmSetupHint": { + "message": "What are the potential security risks?" + }, + "autoConfirmEnabled": { + "message": "Turned on automatic confirmation" + }, + "availableNow": { + "message": "Available now" + }, "accountSecurity": { "message": "Bảo mật tài khoản" }, @@ -4936,7 +4990,7 @@ "message": "Tải xuống Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Tải xuống Bitwarden trên tất cả các thiết bị" + "message": "Tải xuống Bitwarden trên tất cả thiết bị" }, "getTheMobileApp": { "message": "Tải ứng dụng di động" @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Thông tin đăng nhập này có rủi ro và thiếu một trang web. Hãy thêm trang web và đổi mật khẩu để tăng cường bảo mật." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Thiếu trang web" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "Tại sao tôi thấy điều này?" + }, + "resizeSideNavigation": { + "message": "Resize side navigation" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c7f7fbcd618..9530388a4e5 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "项目已发送到归档" }, + "itemWasUnarchived": { + "message": "项目已取消归档" + }, "itemUnarchived": { "message": "项目已取消归档" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "已归档的项目将被排除在一般搜索结果和自动填充建议之外。确定要归档此项目吗?" }, + "archived": { + "message": "Archived" + }, + "unarchiveAndSave": { + "message": "Unarchive and save" + }, "upgradeToUseArchive": { "message": "需要高级会员才能使用归档。" }, + "itemRestored": { + "message": "项目已恢复" + }, "edit": { "message": "编辑" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "专有的两步登录选项,如 YubiKey 和 Duo。" }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "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." + }, + "restartPremium": { + "message": "Restart Premium" + }, "ppremiumSignUpReports": { "message": "密码健康、账户体检以及数据泄露报告,保障您的密码库安全。" }, @@ -2878,7 +2899,7 @@ "message": "排除域名更改已保存" }, "limitSendViews": { - "message": "查看次数限制" + "message": "限制查看次数" }, "limitSendViewsHint": { "message": "达到限额后,任何人无法查看此 Send。", @@ -2967,7 +2988,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "确定要永久删除这个 Send 吗?", + "message": "确定要永久删除此 Send 吗?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -3222,7 +3243,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为 $ACTION$。", + "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为「$ACTION$」。", "placeholders": { "hours": { "content": "$1", @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "管理控制台" }, + "admin": { + "message": "管理员" + }, + "automaticUserConfirmation": { + "message": "自动用户确认" + }, + "automaticUserConfirmationHint": { + "message": "当此设备已解锁时,自动确认待处理的用户" + }, + "autoConfirmOnboardingCallout": { + "message": "通过自动用户确认节省时间" + }, + "autoConfirmWarning": { + "message": "这可能会影响您组织的数据安全。" + }, + "autoConfirmWarningLink": { + "message": "了解此风险" + }, + "autoConfirmSetup": { + "message": "自动确认新用户" + }, + "autoConfirmSetupDesc": { + "message": "当此设备已解锁时,新用户将被自动确认。" + }, + "autoConfirmSetupHint": { + "message": "潜在的安全风险有哪些?" + }, + "autoConfirmEnabled": { + "message": "启用了自动确认" + }, + "availableNow": { + "message": "目前可用" + }, "accountSecurity": { "message": "账户安全" }, @@ -4856,7 +4910,7 @@ "message": "新增" }, "removeItem": { - "message": "删除 $NAME$", + "message": "移除 $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登录存在风险且缺少网站。请添加网站并更改密码以增强安全性。" }, + "vulnerablePassword": { + "message": "易受攻击的密码。" + }, + "changeNow": { + "message": "立即更改" + }, "missingWebsite": { "message": "缺少网站" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "为什么我会看到这个?" + }, + "resizeSideNavigation": { + "message": "调整侧边导航栏大小" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index abb25c48b43..76407d95621 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -573,6 +573,9 @@ "itemWasSentToArchive": { "message": "項目已移至封存" }, + "itemWasUnarchived": { + "message": "已取消封存項目" + }, "itemUnarchived": { "message": "項目取消封存" }, @@ -582,9 +585,18 @@ "archiveItemConfirmDesc": { "message": "封存的項目將不會出現在一般搜尋結果或自動填入建議中。確定要封存此項目嗎?" }, + "archived": { + "message": "已封存" + }, + "unarchiveAndSave": { + "message": "取消封存並儲存" + }, "upgradeToUseArchive": { "message": "需要進階版會員才能使用封存功能。" }, + "itemRestored": { + "message": "已還原項目" + }, "edit": { "message": "編輯" }, @@ -1533,6 +1545,15 @@ "premiumSignUpTwoStepOptions": { "message": "專有的兩步驟登入選項,例如 YubiKey 和 Duo。" }, + "premiumSubscriptionEnded": { + "message": "您的進階版訂閱已到期" + }, + "archivePremiumRestart": { + "message": "若要重新存取您的封存項目,請重新啟用進階版訂閱。若您在重新啟用前編輯封存項目的詳細資料,它將會被移回您的密碼庫。" + }, + "restartPremium": { + "message": "重新啟用進階版" + }, "ppremiumSignUpReports": { "message": "密碼健康度檢查、提供帳戶體檢以及資料外洩報告,以保障您的密碼庫安全。" }, @@ -4808,6 +4829,39 @@ "adminConsole": { "message": "管理控制台" }, + "admin": { + "message": "管理員" + }, + "automaticUserConfirmation": { + "message": "自動使用者確認" + }, + "automaticUserConfirmationHint": { + "message": "在此裝置解鎖時自動確認待處理的使用者" + }, + "autoConfirmOnboardingCallout": { + "message": "透過自動使用者確認節省時間" + }, + "autoConfirmWarning": { + "message": "可能影響您的組織資料安全性。" + }, + "autoConfirmWarningLink": { + "message": "了解風險" + }, + "autoConfirmSetup": { + "message": "自動確認新使用者" + }, + "autoConfirmSetupDesc": { + "message": "當此裝置處於解鎖狀態時,新的使用者將會自動獲得確認。" + }, + "autoConfirmSetupHint": { + "message": "潛在的安全性風險有哪些?" + }, + "autoConfirmEnabled": { + "message": "開啟自動確認" + }, + "availableNow": { + "message": "立即可用" + }, "accountSecurity": { "message": "帳戶安全性" }, @@ -5664,6 +5718,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登入資訊存在風險,且缺少網站。請新增網站並變更密碼以提升安全性。" }, + "vulnerablePassword": { + "message": "有安全疑慮的密碼。" + }, + "changeNow": { + "message": "立即變更" + }, "missingWebsite": { "message": "缺少網站" }, @@ -6039,5 +6099,8 @@ }, "whyAmISeeingThis": { "message": "為什麼我會看到此訊息?" + }, + "resizeSideNavigation": { + "message": "調整側邊欄大小" } } diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index ebabbadf71c..d1380f5eae0 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -8,6 +8,7 @@ import { firstValueFrom, of, BehaviorSubject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { NudgesService } from "@bitwarden/angular/vault"; import { LockService } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ApiService } from "@bitwarden/common/abstractions/api.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"; @@ -124,6 +125,12 @@ describe("AccountSecurityComponent", () => { { provide: ToastService, useValue: mock() }, { provide: UserVerificationService, useValue: mock() }, { provide: ValidationService, useValue: validationService }, + { provide: LockService, useValue: lockService }, + { + provide: AutomaticUserConfirmationService, + useValue: mock(), + }, + { provide: ConfigService, useValue: configService }, { provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService }, ], }) diff --git a/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts new file mode 100644 index 00000000000..4817576a8e0 --- /dev/null +++ b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.spec.ts @@ -0,0 +1,242 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +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 { ActionsService } from "@bitwarden/common/platform/actions"; +import { + ButtonLocation, + SystemNotificationEvent, + SystemNotificationsService, +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { mockAccountInfoWith } from "@bitwarden/common/spec"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +import { ExtensionAuthRequestAnsweringService } from "./extension-auth-request-answering.service"; + +describe("ExtensionAuthRequestAnsweringService", () => { + let accountService: MockProxy; + let authService: MockProxy; + let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$ + let messagingService: MockProxy; + let pendingAuthRequestsState: MockProxy; + let actionService: MockProxy; + let i18nService: MockProxy; + let platformUtilsService: MockProxy; + let systemNotificationsService: MockProxy; + let logService: MockProxy; + + let sut: AuthRequestAnsweringService; + + const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId; + const userAccountInfo = mockAccountInfoWith({ + name: "User", + email: "user@example.com", + }); + const userAccount: Account = { + id: userId, + ...userAccountInfo, + }; + + const authRequestId = "auth-request-id-123"; + + beforeEach(() => { + accountService = mock(); + authService = mock(); + masterPasswordService = { + forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)), + }; + messagingService = mock(); + pendingAuthRequestsState = mock(); + actionService = mock(); + i18nService = mock(); + platformUtilsService = mock(); + systemNotificationsService = mock(); + logService = mock(); + + // Common defaults + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + accountService.activeAccount$ = of(userAccount); + accountService.accounts$ = of({ + [userId]: userAccountInfo, + }); + platformUtilsService.isPopupOpen.mockResolvedValue(false); + i18nService.t.mockImplementation( + (key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`, + ); + systemNotificationsService.create.mockResolvedValue("notif-id"); + + sut = new ExtensionAuthRequestAnsweringService( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + actionService, + i18nService, + platformUtilsService, + systemNotificationsService, + logService, + ); + }); + + describe("receivedPendingAuthRequest()", () => { + it("should throw if authRequestUserId not given", async () => { + // Act + const promise = sut.receivedPendingAuthRequest(undefined, authRequestId); + + // Assert + await expect(promise).rejects.toThrow("authRequestUserId required"); + }); + + it("should throw if authRequestId not given", async () => { + // Act + const promise = sut.receivedPendingAuthRequest(userId, undefined); + + // Assert + await expect(promise).rejects.toThrow("authRequestId required"); + }); + + it("should add a pending marker for the user to state", async () => { + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(pendingAuthRequestsState.add).toHaveBeenCalledTimes(1); + expect(pendingAuthRequestsState.add).toHaveBeenCalledWith(userId); + }); + + describe("given the active user is the intended recipient of the auth request, unlocked, and not required to set/change their master password", () => { + describe("given the popup is open", () => { + it("should send an 'openLoginApproval' message", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval", { + notificationId: authRequestId, + }); + }); + + it("should not create a system notification", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(systemNotificationsService.create).not.toHaveBeenCalled(); + }); + }); + + describe("given the popup is closed", () => { + it("should not send an 'openLoginApproval' message", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested"); + expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com"); + expect(systemNotificationsService.create).toHaveBeenCalledWith({ + id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`, + title: "accountAccessRequested", + body: "confirmAccessAttempt:user@example.com", + buttons: [], + }); + }); + }); + }); + }); + + describe("activeUserMeetsConditionsToShowApprovalDialog()", () => { + describe("given the active user is the intended recipient of the auth request, unlocked, and not required to set/change their master password", () => { + it("should return true if popup is open", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.activeUserMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(true); + }); + + it("should return false if popup is closed", async () => { + // Arrange + platformUtilsService.isPopupOpen.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + const result = await sut.activeUserMeetsConditionsToShowApprovalDialog(userId); + + // Assert + expect(result).toBe(false); + }); + }); + }); + + describe("handleAuthRequestNotificationClicked()", () => { + it("should clear notification and open popup when notification body is clicked", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.NotificationButton, + }; + + // Act + await sut.handleAuthRequestNotificationClicked(event); + + // Assert + expect(systemNotificationsService.clear).toHaveBeenCalledWith({ id: "123" }); + expect(actionService.openPopup).toHaveBeenCalledTimes(1); + }); + + it("should do nothing when an optional notification button is clicked", async () => { + // Arrange + const event: SystemNotificationEvent = { + id: "123", + buttonIdentifier: ButtonLocation.FirstOptionalButton, + }; + + // Act + await sut.handleAuthRequestNotificationClicked(event); + + // Assert + expect(systemNotificationsService.clear).not.toHaveBeenCalled(); + expect(actionService.openPopup).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts new file mode 100644 index 00000000000..988de685978 --- /dev/null +++ b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts @@ -0,0 +1,116 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; +import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +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 { ActionsService } from "@bitwarden/common/platform/actions"; +import { + ButtonLocation, + SystemNotificationEvent, + SystemNotificationsService, +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +export class ExtensionAuthRequestAnsweringService + extends DefaultAuthRequestAnsweringService + implements AuthRequestAnsweringService +{ + constructor( + protected readonly accountService: AccountService, + protected readonly authService: AuthService, + protected readonly masterPasswordService: MasterPasswordServiceAbstraction, + protected readonly messagingService: MessagingService, + protected readonly pendingAuthRequestsState: PendingAuthRequestsStateService, + private readonly actionService: ActionsService, + private readonly i18nService: I18nService, + private readonly platformUtilsService: PlatformUtilsService, + private readonly systemNotificationsService: SystemNotificationsService, + private readonly logService: LogService, + ) { + super( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + ); + } + + async receivedPendingAuthRequest( + authRequestUserId: UserId, + authRequestId: string, + ): Promise { + if (!authRequestUserId) { + throw new Error("authRequestUserId required"); + } + if (!authRequestId) { + throw new Error("authRequestId required"); + } + + // Always persist the pending marker for this user to global state. + await this.pendingAuthRequestsState.add(authRequestUserId); + + const activeUserMeetsConditionsToShowApprovalDialog = + await this.activeUserMeetsConditionsToShowApprovalDialog(authRequestUserId); + + if (activeUserMeetsConditionsToShowApprovalDialog) { + // Send message to open dialog immediately for this request + this.messagingService.send("openLoginApproval", { + // Include the authRequestId so the DeviceManagementComponent can upsert the correct device. + // This will only matter if the user is on the /device-management screen when the auth request is received. + notificationId: authRequestId, + }); + } else { + // Create a system notification + const accounts = await firstValueFrom(this.accountService.accounts$); + const accountInfo = accounts[authRequestUserId]; + + if (!accountInfo) { + this.logService.error("Account not found for authRequestUserId"); + return; + } + + const emailForUser = accountInfo.email; + await this.systemNotificationsService.create({ + id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`, // the underscore is an important delimiter. + title: this.i18nService.t("accountAccessRequested"), + body: this.i18nService.t("confirmAccessAttempt", emailForUser), + buttons: [], + }); + } + } + + async activeUserMeetsConditionsToShowApprovalDialog(authRequestUserId: UserId): Promise { + const meetsBasicConditions = await super.activeUserMeetsConditionsToShowApprovalDialog( + authRequestUserId, + ); + + // To show an approval dialog immediately on Extension, the popup must be open. + const isPopupOpen = await this.platformUtilsService.isPopupOpen(); + const meetsExtensionConditions = meetsBasicConditions && isPopupOpen; + + return meetsExtensionConditions; + } + + /** + * When a system notification is clicked, this function is used to process that event. + * + * @param event The event passed in. Check initNotificationSubscriptions in main.background.ts. + */ + async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise { + if (event.buttonIdentifier === ButtonLocation.NotificationButton) { + await this.systemNotificationsService.clear({ + id: `${event.id}`, + }); + await this.actionService.openPopup(); + } + } +} diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 86cdbffe059..4f55e68fb41 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Subject, switchMap, timer } from "rxjs"; import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; @@ -25,7 +23,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set(); private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map(); private clearLoginCipherFormDataSubject: Subject = new Subject(); - private notificationFallbackTimeout: number | NodeJS.Timeout | null; + private notificationFallbackTimeout: number | NodeJS.Timeout | null = null; private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]); private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = { generatedPasswordFilled: ({ message, sender }) => @@ -63,7 +61,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg sender: chrome.runtime.MessageSender, ) { if (await this.shouldInitAddLoginOrChangePasswordNotification(message, sender)) { - this.websiteOriginsWithFields.set(sender.tab.id, this.getSenderUrlMatchPatterns(sender)); + const tabId = sender.tab?.id; + if (tabId === undefined) { + return; + } + this.websiteOriginsWithFields.set(tabId, this.getSenderUrlMatchPatterns(sender)); this.setupWebRequestsListeners(); } } @@ -80,11 +82,16 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg message: OverlayNotificationsExtensionMessage, sender: chrome.runtime.MessageSender, ) { + const tabId = sender.tab?.id; + if (tabId === undefined) { + return false; + } + return ( (await this.isAddLoginOrChangePasswordNotificationEnabled()) && !(await this.isSenderFromExcludedDomain(sender)) && - message.details?.fields?.length > 0 && - !this.websiteOriginsWithFields.has(sender.tab.id) + (message.details?.fields?.length ?? 0) > 0 && + !this.websiteOriginsWithFields.has(tabId) ); } @@ -107,8 +114,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg */ private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) { return new Set([ - ...generateDomainMatchPatterns(sender.url), - ...generateDomainMatchPatterns(sender.tab.url), + ...(sender.url ? generateDomainMatchPatterns(sender.url) : []), + ...(sender.tab?.url ? generateDomainMatchPatterns(sender.tab.url) : []), ]); } @@ -123,7 +130,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg message: OverlayNotificationsExtensionMessage, sender: chrome.runtime.MessageSender, ) => { - if (!this.websiteOriginsWithFields.has(sender.tab.id)) { + const tabId = sender.tab?.id; + if (tabId === undefined || !this.websiteOriginsWithFields.has(tabId)) { return; } @@ -135,25 +143,24 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg this.clearLoginCipherFormDataSubject.next(); const formData = { uri, username, password, newPassword }; - const existingModifyLoginData = this.modifyLoginCipherFormData.get(sender.tab.id); + const existingModifyLoginData = this.modifyLoginCipherFormData.get(tabId); if (existingModifyLoginData) { formData.username = formData.username || existingModifyLoginData.username; formData.password = formData.password || existingModifyLoginData.password; formData.newPassword = formData.newPassword || existingModifyLoginData.newPassword; } - this.modifyLoginCipherFormData.set(sender.tab.id, formData); + this.modifyLoginCipherFormData.set(tabId, formData); this.clearNotificationFallbackTimeout(); - this.notificationFallbackTimeout = setTimeout( - () => - this.setupNotificationInitTrigger( - sender.tab.id, - "", - this.modifyLoginCipherFormData.get(sender.tab.id), - ).catch((error) => this.logService.error(error)), - 1500, - ); + this.notificationFallbackTimeout = setTimeout(() => { + const modifyLoginData = this.modifyLoginCipherFormData.get(tabId); + if (modifyLoginData) { + this.setupNotificationInitTrigger(tabId, "", modifyLoginData).catch((error) => + this.logService.error(error), + ); + } + }, 1500); }; /** @@ -176,6 +183,10 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg private async isSenderFromExcludedDomain(sender: chrome.runtime.MessageSender): Promise { try { const senderOrigin = sender.origin; + if (!senderOrigin) { + return false; + } + const serverConfig = await this.notificationBackground.getActiveUserServerConfig(); const activeUserVault = serverConfig?.environment?.vault; if (activeUserVault === senderOrigin) { @@ -232,11 +243,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg details: chrome.webRequest.OnBeforeRequestDetails, ): undefined => { if (this.isPostSubmissionFormRedirection(details)) { - this.setupNotificationInitTrigger( - details.tabId, - details.requestId, - this.modifyLoginCipherFormData.get(details.tabId), - ).catch((error) => this.logService.error(error)); + const modifyLoginData = this.modifyLoginCipherFormData.get(details.tabId); + if (modifyLoginData) { + this.setupNotificationInitTrigger(details.tabId, details.requestId, modifyLoginData).catch( + (error) => this.logService.error(error), + ); + } return; } @@ -385,6 +397,10 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg this.clearNotificationFallbackTimeout(); const tab = await BrowserApi.getTab(tabId); + if (!tab) { + return; + } + if (tab.status !== "complete") { await this.delayNotificationInitUntilTabIsComplete(tabId, requestId, modifyLoginData); return; @@ -410,7 +426,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg const handleWebNavigationOnCompleted = async () => { chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted); const tab = await BrowserApi.getTab(tabId); - await this.processNotifications(requestId, modifyLoginData, tab); + if (tab) { + await this.processNotifications(requestId, modifyLoginData, tab); + } }; chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted); }; diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts index 61d6b9dc480..5c2b266f829 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts @@ -4,8 +4,10 @@ import { of } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AUTOFILL_ID, + COPY_IDENTIFIER_ID, COPY_PASSWORD_ID, COPY_USERNAME_ID, COPY_VERIFICATION_CODE_ID, @@ -85,6 +87,7 @@ describe("ContextMenuClickedHandler", () => { accountService = mockAccountServiceWith(mockUserId as UserId); totpService = mock(); eventCollectionService = mock(); + userVerificationService = mock(); sut = new ContextMenuClickedHandler( copyToClipboard, @@ -102,6 +105,93 @@ describe("ContextMenuClickedHandler", () => { afterEach(() => jest.resetAllMocks()); describe("run", () => { + beforeEach(() => { + authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); + userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false); + }); + + const runWithUrl = (data: chrome.contextMenus.OnClickData) => + sut.run(data, { url: "https://test.com" } as any); + + describe("early returns", () => { + it.each([ + { + name: "tab id is missing", + data: createData(COPY_IDENTIFIER_ID), + tab: { url: "https://test.com" } as any, + expectNotCalled: () => expect(copyToClipboard).not.toHaveBeenCalled(), + }, + { + name: "tab url is missing", + data: createData(`${COPY_USERNAME_ID}_${NOOP_COMMAND_SUFFIX}`, COPY_USERNAME_ID), + tab: {} as any, + expectNotCalled: () => { + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + expect(copyToClipboard).not.toHaveBeenCalled(); + }, + }, + ])("returns early when $name", async ({ data, tab, expectNotCalled }) => { + await expect(sut.run(data, tab)).resolves.toBeUndefined(); + expectNotCalled(); + }); + }); + + describe("missing cipher", () => { + it.each([ + { + label: "AUTOFILL", + parentId: AUTOFILL_ID, + extra: () => expect(autofill).not.toHaveBeenCalled(), + }, + { label: "username", parentId: COPY_USERNAME_ID, extra: () => {} }, + { label: "password", parentId: COPY_PASSWORD_ID, extra: () => {} }, + { + label: "totp", + parentId: COPY_VERIFICATION_CODE_ID, + extra: () => expect(totpService.getCode$).not.toHaveBeenCalled(), + }, + ])("breaks silently when cipher is missing for $label", async ({ parentId, extra }) => { + cipherService.getAllDecrypted.mockResolvedValue([]); + + await expect(runWithUrl(createData(`${parentId}_1`, parentId))).resolves.toBeUndefined(); + + expect(copyToClipboard).not.toHaveBeenCalled(); + extra(); + }); + }); + + describe("missing login properties", () => { + it.each([ + { + label: "username", + parentId: COPY_USERNAME_ID, + unset: (c: CipherView): void => (c.login.username = undefined), + }, + { + label: "password", + parentId: COPY_PASSWORD_ID, + unset: (c: CipherView): void => (c.login.password = undefined), + }, + { + label: "totp", + parentId: COPY_VERIFICATION_CODE_ID, + unset: (c: CipherView): void => (c.login.totp = undefined), + isTotp: true, + }, + ])("breaks silently when $label property is missing", async ({ parentId, unset, isTotp }) => { + const cipher = createCipher(); + unset(cipher); + cipherService.getAllDecrypted.mockResolvedValue([cipher]); + + await expect(runWithUrl(createData(`${parentId}_1`, parentId))).resolves.toBeUndefined(); + + expect(copyToClipboard).not.toHaveBeenCalled(); + if (isTotp) { + expect(totpService.getCode$).not.toHaveBeenCalled(); + } + }); + }); + it("can generate password", async () => { await sut.run(createData(GENERATE_PASSWORD_ID), { id: 5 } as any); diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 6f0979d4fd5..aa01ada0838 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -72,6 +70,10 @@ export class ContextMenuClickedHandler { await this.generatePasswordToClipboard(tab); break; case COPY_IDENTIFIER_ID: + if (!tab.id) { + return; + } + this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab }); break; default: @@ -120,6 +122,10 @@ export class ContextMenuClickedHandler { if (isCreateCipherAction) { // pass; defer to logic below } else if (menuItemId === NOOP_COMMAND_SUFFIX) { + if (!tab.url) { + return; + } + const additionalCiphersToGet = info.parentMenuItemId === AUTOFILL_IDENTITY_ID ? [CipherType.Identity] @@ -158,6 +164,10 @@ export class ContextMenuClickedHandler { break; } + if (!cipher) { + break; + } + if (await this.isPasswordRepromptRequired(cipher)) { await openVaultItemPasswordRepromptPopout(tab, { cipherId: cipher.id, @@ -176,6 +186,10 @@ export class ContextMenuClickedHandler { break; } + if (!cipher || !cipher.login?.username) { + break; + } + this.copyToClipboard({ text: cipher.login.username, tab: tab }); break; case COPY_PASSWORD_ID: @@ -184,6 +198,10 @@ export class ContextMenuClickedHandler { break; } + if (!cipher || !cipher.login?.password) { + break; + } + if (await this.isPasswordRepromptRequired(cipher)) { await openVaultItemPasswordRepromptPopout(tab, { cipherId: cipher.id, @@ -205,6 +223,10 @@ export class ContextMenuClickedHandler { break; } + if (!cipher || !cipher.login?.totp) { + break; + } + if (await this.isPasswordRepromptRequired(cipher)) { await openVaultItemPasswordRepromptPopout(tab, { cipherId: cipher.id, @@ -240,9 +262,10 @@ export class ContextMenuClickedHandler { } private async getIdentifier(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) { + const tabId = tab.id!; return new Promise((resolve, reject) => { BrowserApi.sendTabsMessage( - tab.id, + tabId, { command: "getClickedElement" }, { frameId: info.frameId }, (identifier: string) => { diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.ts index 03816f2b382..9257ac748bb 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-content-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AssertCredentialParams, CreateCredentialParams, @@ -41,12 +39,12 @@ import { MessageWithMetadata, Messenger } from "./messaging/messenger"; */ async function handleFido2Message( message: MessageWithMetadata, - abortController: AbortController, + abortController?: AbortController, ) { const requestId = Date.now().toString(); const abortHandler = () => sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId }); - abortController.signal.addEventListener("abort", abortHandler); + abortController?.signal.addEventListener("abort", abortHandler); try { if (message.type === MessageTypes.CredentialCreationRequest) { @@ -67,7 +65,7 @@ import { MessageWithMetadata, Messenger } from "./messaging/messenger"; return sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId }); } } finally { - abortController.signal.removeEventListener("abort", abortHandler); + abortController?.signal.removeEventListener("abort", abortHandler); } } diff --git a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts index 0cccd91876d..07ffa553b07 100644 --- a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts +++ b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CreateCredentialResult, AssertCredentialResult, diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index b61e5e19d53..c2f872d7ba5 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { InlineMenuElementPosition, InlineMenuPosition, @@ -62,8 +60,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte */ private inlineMenuEnabled = true; private mutationObserverIterations = 0; - private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; - private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout; + private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout | null = null; + private handlePersistentLastChildOverrideTimeout: number | NodeJS.Timeout | null = null; private lastElementOverrides: WeakMap = new WeakMap(); private readonly customElementDefaultStyles: Partial = { all: "initial", @@ -77,7 +75,21 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte }; constructor() { - this.setupMutationObserver(); + /** + * Sets up mutation observers for the inline menu elements, the menu container, and + * the document element. The mutation observers are used to remove any styles that + * are added to the inline menu elements by the website. They are also used to ensure + * that the inline menu elements are always present at the bottom of the menu container. + */ + this.htmlMutationObserver = new MutationObserver(this.handlePageMutations); + this.bodyMutationObserver = new MutationObserver(this.handlePageMutations); + this.inlineMenuElementsMutationObserver = new MutationObserver( + this.handleInlineMenuElementMutationObserverUpdate, + ); + this.containerElementMutationObserver = new MutationObserver( + this.handleContainerElementMutationObserverUpdate, + ); + this.observePageAttributes(); } /** @@ -181,12 +193,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * Updates the position of the inline menu button. */ private async appendButtonElement(): Promise { - if (!this.inlineMenuEnabled) { - return; - } - if (!this.buttonElement) { - this.createButtonElement(); + this.buttonElement = this.createButtonElement(); this.updateCustomElementDefaultStyles(this.buttonElement); } @@ -201,12 +209,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * Updates the position of the inline menu list. */ private async appendListElement(): Promise { - if (!this.inlineMenuEnabled) { - return; - } - if (!this.listElement) { - this.createListElement(); + this.listElement = this.createListElement(); this.updateCustomElementDefaultStyles(this.listElement); } @@ -257,16 +261,12 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * to create the element if it already exists in the DOM. */ private createButtonElement() { - if (!this.inlineMenuEnabled) { - return; - } - if (this.isFirefoxBrowser) { this.buttonElement = globalThis.document.createElement("div"); this.buttonElement.setAttribute("popover", "manual"); new AutofillInlineMenuButtonIframe(this.buttonElement); - return; + return this.buttonElement; } const customElementName = this.generateRandomCustomElementName(); @@ -282,6 +282,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.buttonElement = globalThis.document.createElement(customElementName); this.buttonElement.setAttribute("popover", "manual"); + return this.buttonElement; } /** @@ -289,16 +290,12 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte * to create the element if it already exists in the DOM. */ private createListElement() { - if (!this.inlineMenuEnabled) { - return; - } - if (this.isFirefoxBrowser) { this.listElement = globalThis.document.createElement("div"); this.listElement.setAttribute("popover", "manual"); new AutofillInlineMenuListIframe(this.listElement); - return; + return this.listElement; } const customElementName = this.generateRandomCustomElementName(); @@ -314,6 +311,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.listElement = globalThis.document.createElement(customElementName); this.listElement.setAttribute("popover", "manual"); + return this.listElement; } /** @@ -330,27 +328,6 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.observeCustomElements(); } - /** - * Sets up mutation observers for the inline menu elements, the menu container, and - * the document element. The mutation observers are used to remove any styles that - * are added to the inline menu elements by the website. They are also used to ensure - * that the inline menu elements are always present at the bottom of the menu container. - */ - private setupMutationObserver = () => { - this.htmlMutationObserver = new MutationObserver(this.handlePageMutations); - this.bodyMutationObserver = new MutationObserver(this.handlePageMutations); - - this.inlineMenuElementsMutationObserver = new MutationObserver( - this.handleInlineMenuElementMutationObserverUpdate, - ); - - this.containerElementMutationObserver = new MutationObserver( - this.handleContainerElementMutationObserverUpdate, - ); - - this.observePageAttributes(); - }; - /** * Sets up mutation observers to verify that the inline menu * elements are not modified by the website. @@ -652,6 +629,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte return; } + if (!this.buttonElement) { + return; + } + const lastChild = containerElement.lastElementChild; const secondToLastChild = lastChild?.previousElementSibling; const lastChildIsInlineMenuList = lastChild === this.listElement; @@ -667,7 +648,8 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte this.lastElementOverrides.set(lastChild, lastChildEncounterCount + 1); } - if (this.lastElementOverrides.get(lastChild) >= 3) { + const lastChildEncounterCountAfterUpdate = this.lastElementOverrides.get(lastChild) || 0; + if (lastChildEncounterCountAfterUpdate >= 3) { this.handlePersistentLastChildOverride(lastChild); return; @@ -686,6 +668,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte (lastChildIsInlineMenuList && !secondToLastChildIsInlineMenuButton) || (lastChildIsInlineMenuButton && isInlineMenuListVisible) ) { + if (!this.listElement) { + return; + } containerElement.insertBefore(this.buttonElement, this.listElement); return; } diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index adabae2c31d..ace314c6a84 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; @@ -33,13 +31,10 @@ export class Fido2CipherRowComponent { @Output() onSelected = new EventEmitter(); // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() cipher: CipherView; + @Input({ required: true }) cipher!: CipherView; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() last: boolean; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() title: string; + @Input({ required: true }) title!: string; protected selectCipher(c: CipherView) { this.onSelected.emit(c); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 817a7cca43c..7ea89e114ab 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1086,15 +1086,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ pageDetails, ) ) { - const hasUsernameField = [...this.formFieldElements.values()].some((field) => - this.inlineMenuFieldQualificationService.isUsernameField(field), - ); - - if (hasUsernameField) { - void this.setQualifiedLoginFillType(autofillFieldData); - } else { - this.setQualifiedAccountCreationFillType(autofillFieldData); - } + this.setQualifiedAccountCreationFillType(autofillFieldData); return false; } @@ -1659,17 +1651,19 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ return false; }; const scrollHandler = this.useEventHandlersMemo( - throttle(async (event) => { + throttle(async (event: Event) => { + const scrollY = globalThis.scrollY; + const scrollX = globalThis.scrollX; if ( - currentScrollY !== globalThis.scrollY || - currentScrollX !== globalThis.scrollX || - eventTargetContainsFocusedField(event.target) + currentScrollY !== scrollY || + currentScrollX !== scrollX || + (event.target instanceof Element && eventTargetContainsFocusedField(event.target)) ) { repositionHandler(event); } - currentScrollY = globalThis.scrollY; - currentScrollX = globalThis.scrollX; + currentScrollY = scrollY; + currentScrollX = scrollX; }, 50), AUTOFILL_OVERLAY_HANDLE_SCROLL, ); diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index a3d61c7f0b2..dc07ca1e258 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FieldRect } from "../background/abstractions/overlay.background"; import { AutofillPort } from "../enums/autofill-port.enum"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; @@ -144,11 +142,14 @@ export function setElementStyles( } for (const styleProperty in styles) { - element.style.setProperty( - styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case - styles[styleProperty], - priority ? "important" : undefined, - ); + const styleValue = styles[styleProperty]; + if (styleValue !== undefined) { + element.style.setProperty( + styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case + styleValue, + priority ? "important" : undefined, + ); + } } } @@ -175,12 +176,13 @@ export function setupExtensionDisconnectAction(callback: (port: chrome.runtime.P * @param windowContext - The global window context */ export function setupAutofillInitDisconnectAction(windowContext: Window) { - if (!windowContext.bitwardenAutofillInit) { + const bitwardenAutofillInit = windowContext.bitwardenAutofillInit; + if (!bitwardenAutofillInit) { return; } const onDisconnectCallback = () => { - windowContext.bitwardenAutofillInit.destroy(); + bitwardenAutofillInit.destroy(); delete windowContext.bitwardenAutofillInit; }; setupExtensionDisconnectAction(onDisconnectCallback); @@ -357,7 +359,7 @@ export function getAttributeBoolean( */ export function getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { if (attributeName in element) { - return (element as FormElementWithAttribute)[attributeName]; + return (element as FormElementWithAttribute)[attributeName] ?? null; } return element.getAttribute(attributeName); @@ -369,9 +371,12 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri * @param callback - The callback function to throttle. * @param limit - The time in milliseconds to throttle the callback. */ -export function throttle(callback: (_args: any) => any, limit: number) { +export function throttle unknown>( + callback: FunctionType, + limit: number, +): (this: ThisParameterType, ...args: Parameters) => void { let waitingDelay = false; - return function (...args: unknown[]) { + return function (this: ThisParameterType, ...args: Parameters) { if (!waitingDelay) { callback.apply(this, args); waitingDelay = true; @@ -387,9 +392,14 @@ export function throttle(callback: (_args: any) => any, limit: number) { * @param delay - The time in milliseconds to debounce the callback. * @param immediate - Determines whether the callback should run immediately. */ -export function debounce(callback: (_args: any) => any, delay: number, immediate?: boolean) { - let timeout: NodeJS.Timeout; - return function (...args: unknown[]) { +export function debounce unknown>( + callback: FunctionType, + delay: number, + immediate?: boolean, +): (this: ThisParameterType, ...args: Parameters) => void { + let timeout: ReturnType | null = null; + + return function (this: ThisParameterType, ...args: Parameters) { const callImmediately = !!immediate && !timeout; if (timeout) { @@ -430,16 +440,17 @@ export function getSubmitButtonKeywordsSet(element: HTMLElement): Set { const keywordsSet = new Set(); for (let i = 0; i < keywords.length; i++) { - if (typeof keywords[i] === "string") { + const keyword = keywords[i]; + if (typeof keyword === "string") { // Iterate over all keywords metadata and split them by non-letter characters. // This ensures we check against individual words and not the entire string. - keywords[i] + keyword .toLowerCase() .replace(/[-\s]/g, "") .split(/[^\p{L}]+/gu) - .forEach((keyword) => { - if (keyword) { - keywordsSet.add(keyword); + .forEach((splitKeyword) => { + if (splitKeyword) { + keywordsSet.add(splitKeyword); } }); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4ea59b8de07..2fd1bc97459 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -40,7 +40,7 @@ import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/p import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; @@ -52,7 +52,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@ import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; -import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; @@ -275,6 +274,7 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { ExtensionAuthRequestAnsweringService } from "../auth/services/auth-request-answering/extension-auth-request-answering.service"; import { AuthStatusBadgeUpdaterService } from "../auth/services/auth-status-badge-updater.service"; import { ExtensionLockService } from "../auth/services/extension-lock.service"; import { OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface } from "../autofill/background/abstractions/overlay-notifications.background"; @@ -392,7 +392,7 @@ export default class MainBackground { serverNotificationsService: ServerNotificationsService; systemNotificationService: SystemNotificationsService; actionsService: ActionsService; - authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction; + authRequestAnsweringService: AuthRequestAnsweringService; stateService: StateServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction; @@ -1211,16 +1211,17 @@ export default class MainBackground { this.pendingAuthRequestStateService = new PendingAuthRequestsStateService(this.stateProvider); - this.authRequestAnsweringService = new AuthRequestAnsweringService( + this.authRequestAnsweringService = new ExtensionAuthRequestAnsweringService( this.accountService, - this.actionsService, this.authService, - this.i18nService, this.masterPasswordService, this.messagingService, this.pendingAuthRequestStateService, + this.actionsService, + this.i18nService, this.platformUtilsService, this.systemNotificationService, + this.logService, ); this.serverNotificationsService = new DefaultServerNotificationsService( @@ -1512,6 +1513,7 @@ export default class MainBackground { this.accountService, this.billingAccountProfileStateService, this.configService, + this.logService, this.organizationService, this.platformUtilsService, this.stateProvider, diff --git a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts index 262d6cf833b..4cd155c8ae3 100644 --- a/apps/browser/src/dirt/phishing-detection/phishing-resources.ts +++ b/apps/browser/src/dirt/phishing-detection/phishing-resources.ts @@ -18,8 +18,7 @@ export const PHISHING_RESOURCES: Record + new Promise((resolve) => jest.requireActual("timers").setImmediate(resolve)); + +// [FIXME] Move mocking and compression helpers to a shared test utils library +// to separate from phishing data service tests. +export const setupPhishingMocks = (mockedResult: string | ArrayBuffer = "mocked-data") => { + // Store original globals + const originals = { + Response: global.Response, + CompressionStream: global.CompressionStream, + DecompressionStream: global.DecompressionStream, + Blob: global.Blob, + atob: global.atob, + btoa: global.btoa, + }; + + // Mock missing or browser-only globals + global.atob = (str) => Buffer.from(str, "base64").toString("binary"); + global.btoa = (str) => Buffer.from(str, "binary").toString("base64"); + + (global as any).CompressionStream = class {}; + (global as any).DecompressionStream = class {}; + + global.Blob = class { + constructor(public parts: any[]) {} + stream() { + return { pipeThrough: () => ({}) }; + } + } as any; + + global.Response = class { + body = { pipeThrough: () => ({}) }; + // Return string for decompression + text() { + return Promise.resolve(typeof mockedResult === "string" ? mockedResult : ""); + } + // Return ArrayBuffer for compression + arrayBuffer() { + if (typeof mockedResult === "string") { + const bytes = new TextEncoder().encode(mockedResult); + return Promise.resolve(bytes.buffer); + } + + return Promise.resolve(mockedResult); + } + } as any; + + // Cleanup function + return () => { + Object.assign(global, originals); + }; +}; describe("PhishingDataService", () => { let service: PhishingDataService; @@ -17,17 +76,30 @@ describe("PhishingDataService", () => { let taskSchedulerService: TaskSchedulerService; let logService: MockProxy; let platformUtilsService: MockProxy; - const stateProvider: FakeGlobalStateProvider = new FakeGlobalStateProvider(); + const fakeGlobalStateProvider: FakeGlobalStateProvider = new FakeGlobalStateProvider(); - const setMockState = (state: PhishingData) => { - stateProvider.getFake(PHISHING_DOMAINS_KEY).stateSubject.next(state); + const setMockMeta = (state: PhishingDataMeta) => { + fakeGlobalStateProvider.getFake(PHISHING_DOMAINS_META_KEY).stateSubject.next(state); + return state; + }; + const setMockBlob = (state: PhishingDataBlob) => { + fakeGlobalStateProvider.getFake(PHISHING_DOMAINS_BLOB_KEY).stateSubject.next(state); return state; }; let fetchChecksumSpy: jest.SpyInstance; - let fetchWebAddressesSpy: jest.SpyInstance; + let fetchAndCompressSpy: jest.SpyInstance; - beforeEach(() => { + const mockMeta: PhishingDataMeta = { + checksum: "abc", + timestamp: Date.now(), + applicationVersion: "1.0.0", + }; + const mockBlob = "http://phish.com\nhttps://badguy.net"; + const mockCompressedBlob = + "H4sIAAAAAAAA/8vMTSzJzM9TSE7MLchJLElVyE9TyC9KSS1S0FFIz8hLz0ksSQUAtK7XMSYAAAA="; + + beforeEach(async () => { jest.useFakeTimers(); apiService = mock(); logService = mock(); @@ -40,54 +112,75 @@ describe("PhishingDataService", () => { service = new PhishingDataService( apiService, taskSchedulerService, - stateProvider, + fakeGlobalStateProvider, logService, platformUtilsService, ); - fetchChecksumSpy = jest.spyOn(service as any, "fetchPhishingChecksum"); - fetchWebAddressesSpy = jest.spyOn(service as any, "fetchPhishingWebAddresses"); + fetchAndCompressSpy = jest.spyOn(service as any, "fetchAndCompress"); + + fetchChecksumSpy.mockResolvedValue("new-checksum"); + fetchAndCompressSpy.mockResolvedValue("compressed-blob"); + }); + + describe("initialization", () => { + beforeEach(() => { + jest.spyOn(service as any, "_compressString").mockResolvedValue(mockCompressedBlob); + jest.spyOn(service as any, "_decompressString").mockResolvedValue(mockBlob); + }); + + it("should perform background update", async () => { + platformUtilsService.getApplicationVersion.mockResolvedValue("1.0.x"); + jest + .spyOn(service as any, "getNextWebAddresses") + .mockResolvedValue({ meta: mockMeta, blob: mockBlob }); + + setMockBlob(mockBlob); + setMockMeta(mockMeta); + + const sub = service.update$.subscribe(); + await flushPromises(); + + const url = new URL("http://phish.com"); + const QAurl = new URL("http://phishing.testcategory.com"); + expect(await service.isPhishingWebAddress(url)).toBe(true); + expect(await service.isPhishingWebAddress(QAurl)).toBe(true); + + sub.unsubscribe(); + }); }); describe("isPhishingWebAddress", () => { + beforeEach(() => { + jest.spyOn(service as any, "_compressString").mockResolvedValue(mockCompressedBlob); + jest.spyOn(service as any, "_decompressString").mockResolvedValue(mockBlob); + }); + it("should detect a phishing web address", async () => { - setMockState({ - webAddresses: ["phish.com", "badguy.net"], - timestamp: Date.now(), - checksum: "abc123", - applicationVersion: "1.0.0", - }); + service["_webAddressesSet"] = new Set(["phish.com", "badguy.net"]); + const url = new URL("http://phish.com"); const result = await service.isPhishingWebAddress(url); + expect(result).toBe(true); }); it("should not detect a safe web address", async () => { - setMockState({ - webAddresses: ["phish.com", "badguy.net"], - timestamp: Date.now(), - checksum: "abc123", - applicationVersion: "1.0.0", - }); + service["_webAddressesSet"] = new Set(["phish.com", "badguy.net"]); const url = new URL("http://safe.com"); const result = await service.isPhishingWebAddress(url); expect(result).toBe(false); }); it("should match against root web address", async () => { - setMockState({ - webAddresses: ["phish.com", "badguy.net"], - timestamp: Date.now(), - checksum: "abc123", - applicationVersion: "1.0.0", - }); + service["_webAddressesSet"] = new Set(["phish.com", "badguy.net"]); const url = new URL("http://phish.com/about"); const result = await service.isPhishingWebAddress(url); expect(result).toBe(true); }); it("should not error on empty state", async () => { - setMockState(undefined as any); + service["_webAddressesSet"] = null; const url = new URL("http://phish.com/about"); const result = await service.isPhishingWebAddress(url); expect(result).toBe(false); @@ -95,64 +188,142 @@ describe("PhishingDataService", () => { }); describe("getNextWebAddresses", () => { + beforeEach(() => { + jest.spyOn(service as any, "_compressString").mockResolvedValue(mockCompressedBlob); + jest.spyOn(service as any, "_decompressString").mockResolvedValue(mockBlob); + }); + it("refetches all web addresses if applicationVersion has changed", async () => { - const prev: PhishingData = { - webAddresses: ["a.com"], + const prev: PhishingDataMeta = { timestamp: Date.now() - 60000, checksum: "old", applicationVersion: "1.0.0", }; fetchChecksumSpy.mockResolvedValue("new"); - fetchWebAddressesSpy.mockResolvedValue(["d.com", "e.com"]); platformUtilsService.getApplicationVersion.mockResolvedValue("2.0.0"); const result = await service.getNextWebAddresses(prev); - expect(result!.webAddresses).toEqual(["d.com", "e.com"]); - expect(result!.checksum).toBe("new"); - expect(result!.applicationVersion).toBe("2.0.0"); + expect(result!.blob).toBe("compressed-blob"); + expect(result!.meta!.checksum).toBe("new"); + expect(result!.meta!.applicationVersion).toBe("2.0.0"); }); - it("only updates timestamp if checksum matches", async () => { - const prev: PhishingData = { - webAddresses: ["a.com"], - timestamp: Date.now() - 60000, + it("returns null when checksum matches and cache not expired", async () => { + const prev: PhishingDataMeta = { + timestamp: Date.now(), checksum: "abc", applicationVersion: "1.0.0", }; fetchChecksumSpy.mockResolvedValue("abc"); const result = await service.getNextWebAddresses(prev); - expect(result!.webAddresses).toEqual(prev.webAddresses); - expect(result!.checksum).toBe("abc"); - expect(result!.timestamp).not.toBe(prev.timestamp); + expect(result).toBeNull(); }); - it("patches daily domains if cache is fresh", async () => { - const prev: PhishingData = { - webAddresses: ["a.com"], - timestamp: Date.now() - 60000, + it("patches daily domains when cache is expired and checksum unchanged", async () => { + const prev: PhishingDataMeta = { + timestamp: 0, + checksum: "old", + applicationVersion: "1.0.0", + }; + const dailyLines = ["b.com", "c.com"]; + fetchChecksumSpy.mockResolvedValue("old"); + jest.spyOn(service as any, "fetchText").mockResolvedValue(dailyLines); + + setMockBlob(mockBlob); + + const expectedBlob = + "H4sIAAAAAAAA/8vMTSzJzM9TSE7MLchJLElVyE9TyC9KSS1S0FFIz8hLz0ksSQUAtK7XMSYAAAA="; + const result = await service.getNextWebAddresses(prev); + + expect(result!.blob).toBe(expectedBlob); + expect(result!.meta!.checksum).toBe("old"); + }); + + it("fetches all domains when checksum has changed", async () => { + const prev: PhishingDataMeta = { + timestamp: 0, checksum: "old", applicationVersion: "1.0.0", }; fetchChecksumSpy.mockResolvedValue("new"); - fetchWebAddressesSpy.mockResolvedValue(["b.com", "c.com"]); + fetchAndCompressSpy.mockResolvedValue("new-blob"); const result = await service.getNextWebAddresses(prev); - expect(result!.webAddresses).toEqual(["a.com", "b.com", "c.com"]); - expect(result!.checksum).toBe("new"); + expect(result!.blob).toBe("new-blob"); + expect(result!.meta!.checksum).toBe("new"); + }); + }); + + describe("compression helpers", () => { + let restore: () => void; + + beforeEach(async () => { + restore = setupPhishingMocks("abc"); }); - it("fetches all domains if cache is old", async () => { - const prev: PhishingData = { - webAddresses: ["a.com"], - timestamp: Date.now() - 2 * 24 * 60 * 60 * 1000, - checksum: "old", - applicationVersion: "1.0.0", - }; - fetchChecksumSpy.mockResolvedValue("new"); - fetchWebAddressesSpy.mockResolvedValue(["d.com", "e.com"]); - const result = await service.getNextWebAddresses(prev); - expect(result!.webAddresses).toEqual(["d.com", "e.com"]); - expect(result!.checksum).toBe("new"); + afterEach(() => { + if (restore) { + restore(); + } + delete (Uint8Array as any).fromBase64; + jest.restoreAllMocks(); + }); + + describe("_compressString", () => { + it("compresses a string to base64", async () => { + const out = await service["_compressString"]("abc"); + expect(out).toBe("YWJj"); // base64 for 'abc' + }); + + it("compresses using fallback on older browsers", async () => { + const input = "abc"; + const expected = btoa(encodeURIComponent(input)); + const out = await service["_compressString"](input); + expect(out).toBe(expected); + }); + + it("compresses using btoa on error", async () => { + const input = "abc"; + const expected = btoa(encodeURIComponent(input)); + const out = await service["_compressString"](input); + expect(out).toBe(expected); + }); + }); + describe("_decompressString", () => { + it("decompresses a string from base64", async () => { + const base64 = btoa("ignored"); + const out = await service["_decompressString"](base64); + expect(out).toBe("abc"); + }); + + it("decompresses using fallback on older browsers", async () => { + // Provide a fromBase64 implementation + (Uint8Array as any).fromBase64 = (b64: string) => new Uint8Array([100, 101, 102]); + + const out = await service["_decompressString"]("ignored"); + expect(out).toBe("abc"); + }); + + it("decompresses using atob on error", async () => { + const base64 = btoa(encodeURIComponent("abc")); + const out = await service["_decompressString"](base64); + expect(out).toBe("abc"); + }); + }); + }); + + describe("_loadBlobToMemory", () => { + it("loads blob into memory set", async () => { + const prevBlob = "ignored-base64"; + fakeGlobalStateProvider.getFake(PHISHING_DOMAINS_BLOB_KEY).stateSubject.next(prevBlob); + + jest.spyOn(service as any, "_decompressString").mockResolvedValue("phish.com\nbadguy.net"); + + await service["_loadBlobToMemory"](); + const set = service["_webAddressesSet"] as Set; + expect(set).toBeDefined(); + expect(set.has("phish.com")).toBe(true); + expect(set.has("badguy.net")).toBe(true); }); }); }); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts index 21fe74f1873..85e91b06a6b 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts @@ -3,14 +3,11 @@ import { EMPTY, first, firstValueFrom, - map, - retry, share, startWith, Subject, switchMap, tap, - timer, } from "rxjs"; import { devFlagEnabled, devFlagValue } from "@bitwarden/browser/platform/flags"; @@ -22,11 +19,14 @@ import { GlobalStateProvider, KeyDefinition, PHISHING_DETECTION_DISK } from "@bi import { getPhishingResources, PhishingResourceType } from "../phishing-resources"; -export type PhishingData = { - webAddresses: string[]; - timestamp: number; +/** + * Metadata about the phishing data set + */ +export type PhishingDataMeta = { + /** The last known checksum of the phishing data set */ checksum: string; - + /** The last time the data set was updated */ + timestamp: number; /** * We store the application version to refetch the entire dataset on a new client release. * This counteracts daily appends updates not removing inactive or false positive web addresses. @@ -34,30 +34,42 @@ export type PhishingData = { applicationVersion: string; }; -export const PHISHING_DOMAINS_KEY = new KeyDefinition( +/** + * The phishing data blob is a string representation of the phishing web addresses + */ +export type PhishingDataBlob = string; +export type PhishingData = { meta: PhishingDataMeta; blob: PhishingDataBlob }; + +export const PHISHING_DOMAINS_META_KEY = new KeyDefinition( PHISHING_DETECTION_DISK, - "phishingDomains", + "phishingDomainsMeta", { - deserializer: (value: PhishingData) => - value ?? { webAddresses: [], timestamp: 0, checksum: "", applicationVersion: "" }, + deserializer: (value: PhishingDataMeta) => { + return { + checksum: value?.checksum ?? "", + timestamp: value?.timestamp ?? 0, + applicationVersion: value?.applicationVersion ?? "", + }; + }, + }, +); + +export const PHISHING_DOMAINS_BLOB_KEY = new KeyDefinition( + PHISHING_DETECTION_DISK, + "phishingDomainsBlob", + { + deserializer: (value: string) => value ?? "", }, ); /** Coordinates fetching, caching, and patching of known phishing web addresses */ export class PhishingDataService { - private _testWebAddresses = this.getTestWebAddresses(); - private _cachedState = this.globalStateProvider.get(PHISHING_DOMAINS_KEY); - private _webAddresses$ = this._cachedState.state$.pipe( - map( - (state) => - new Set( - (state?.webAddresses?.filter((line) => line.trim().length > 0) ?? []).concat( - this._testWebAddresses, - "phishing.testcategory.com", // Included for QA to test in prod - ), - ), - ), - ); + private _testWebAddresses = this.getTestWebAddresses().concat("phishing.testcategory.com"); // Included for QA to test in prod + private _phishingMetaState = this.globalStateProvider.get(PHISHING_DOMAINS_META_KEY); + private _phishingBlobState = this.globalStateProvider.get(PHISHING_DOMAINS_BLOB_KEY); + + // In-memory set loaded from blob for fast lookups without reading large storage repeatedly + private _webAddressesSet: Set | null = null; // How often are new web addresses added to the remote? readonly UPDATE_INTERVAL_DURATION = 24 * 60 * 60 * 1000; // 24 hours @@ -65,39 +77,17 @@ export class PhishingDataService { private _triggerUpdate$ = new Subject(); update$ = this._triggerUpdate$.pipe( startWith(undefined), // Always emit once - tap(() => this.logService.info(`[PhishingDataService] Update triggered...`)), switchMap(() => - this._cachedState.state$.pipe( + this._phishingMetaState.state$.pipe( first(), // Only take the first value to avoid an infinite loop when updating the cache below - switchMap(async (cachedState) => { - const next = await this.getNextWebAddresses(cachedState); - if (next) { - await this._cachedState.update(() => next); - this.logService.info(`[PhishingDataService] cache updated`); - } + tap((metaState) => { + // Perform any updates in the background if needed + void this._backgroundUpdate(metaState); }), - retry({ - count: 3, - delay: (err, count) => { - this.logService.error( - `[PhishingDataService] Unable to update web addresses. Attempt ${count}.`, - err, - ); - return timer(5 * 60 * 1000); // 5 minutes - }, - resetOnSuccess: true, + catchError((err: unknown) => { + this.logService.error("[PhishingDataService] Background update failed to start.", err); + return EMPTY; }), - catchError( - ( - err: unknown /** Eslint actually crashed if you remove this type: https://github.com/cartant/eslint-plugin-rxjs/issues/122 */, - ) => { - this.logService.error( - "[PhishingDataService] Retries unsuccessful. Unable to update web addresses.", - err, - ); - return EMPTY; - }, - ), ), ), share(), @@ -111,6 +101,7 @@ export class PhishingDataService { private platformUtilsService: PlatformUtilsService, private resourceType: PhishingResourceType = PhishingResourceType.Links, ) { + this.logService.debug("[PhishingDataService] Initializing service..."); this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.phishingDomainUpdate, () => { this._triggerUpdate$.next(); }); @@ -118,6 +109,7 @@ export class PhishingDataService { ScheduledTaskNames.phishingDomainUpdate, this.UPDATE_INTERVAL_DURATION, ); + void this._loadBlobToMemory(); } /** @@ -127,12 +119,17 @@ export class PhishingDataService { * @returns True if the URL is a known phishing web address, false otherwise */ async isPhishingWebAddress(url: URL): Promise { - // Use domain (hostname) matching for domain resources, and link matching for links resources - const entries = await firstValueFrom(this._webAddresses$); + if (!this._webAddressesSet) { + this.logService.debug("[PhishingDataService] Set not loaded; skipping check"); + return false; + } + const set = this._webAddressesSet!; const resource = getPhishingResources(this.resourceType); - if (resource && resource.match) { - for (const entry of entries) { + + // Custom matcher per resource + if (resource && resource?.match) { + for (const entry of set) { if (resource.match(url, entry)) { return true; } @@ -140,54 +137,59 @@ export class PhishingDataService { return false; } - // Default/domain behavior: exact hostname match as a fallback - return entries.has(url.hostname); + // Default set-based lookup + return set.has(url.hostname); } - async getNextWebAddresses(prev: PhishingData | null): Promise { - prev = prev ?? { webAddresses: [], timestamp: 0, checksum: "", applicationVersion: "" }; - const timestamp = Date.now(); - const prevAge = timestamp - prev.timestamp; - this.logService.info(`[PhishingDataService] Cache age: ${prevAge}`); + async getNextWebAddresses( + previous: PhishingDataMeta | null, + ): Promise | null> { + const prevMeta = previous ?? { timestamp: 0, checksum: "", applicationVersion: "" }; + const now = Date.now(); + // Updates to check const applicationVersion = await this.platformUtilsService.getApplicationVersion(); - - // If checksum matches, return existing data with new timestamp & version const remoteChecksum = await this.fetchPhishingChecksum(this.resourceType); - if (remoteChecksum && prev.checksum === remoteChecksum) { - this.logService.info( - `[PhishingDataService] Remote checksum matches local checksum, updating timestamp only.`, - ); - return { ...prev, timestamp, applicationVersion }; - } - // Checksum is different, data needs to be updated. - // Approach 1: Fetch only new web addresses and append - const isOneDayOldMax = prevAge <= this.UPDATE_INTERVAL_DURATION; - if (isOneDayOldMax && applicationVersion === prev.applicationVersion) { - const webAddressesTodayUrl = getPhishingResources(this.resourceType)!.todayUrl; - const dailyWebAddresses: string[] = - await this.fetchPhishingWebAddresses(webAddressesTodayUrl); - this.logService.info( - `[PhishingDataService] ${dailyWebAddresses.length} new phishing web addresses added`, - ); + // Logic checks + const appVersionChanged = applicationVersion !== prevMeta.applicationVersion; + const masterChecksumChanged = remoteChecksum !== prevMeta.checksum; + + // Check for full updated + if (masterChecksumChanged || appVersionChanged) { + this.logService.info("[PhishingDataService] Checksum or version changed; Fetching ALL."); + const remoteUrl = getPhishingResources(this.resourceType)!.remoteUrl; + const blob = await this.fetchAndCompress(remoteUrl); return { - webAddresses: prev.webAddresses.concat(dailyWebAddresses), - checksum: remoteChecksum, - timestamp, - applicationVersion, + blob, + meta: { checksum: remoteChecksum, timestamp: now, applicationVersion }, }; } - // Approach 2: Fetch all web addresses - const remoteUrl = getPhishingResources(this.resourceType)!.remoteUrl; - const remoteWebAddresses = await this.fetchPhishingWebAddresses(remoteUrl); - return { - webAddresses: remoteWebAddresses, - timestamp, - checksum: remoteChecksum, - applicationVersion, - }; + // Check for daily file + const isCacheExpired = now - prevMeta.timestamp > this.UPDATE_INTERVAL_DURATION; + + if (isCacheExpired) { + this.logService.info("[PhishingDataService] Daily cache expired; Fetching TODAY's"); + const url = getPhishingResources(this.resourceType)!.todayUrl; + const newLines = await this.fetchText(url); + const prevBlob = (await firstValueFrom(this._phishingBlobState.state$)) ?? ""; + const oldText = prevBlob ? await this._decompressString(prevBlob) : ""; + + // Join the new lines to the existing list + const combined = (oldText ? oldText + "\n" : "") + newLines.join("\n"); + + return { + blob: await this._compressString(combined), + meta: { + checksum: remoteChecksum, + timestamp: now, // Reset the timestamp + applicationVersion, + }, + }; + } + + return null; } private async fetchPhishingChecksum(type: PhishingResourceType = PhishingResourceType.Domains) { @@ -198,8 +200,24 @@ export class PhishingDataService { } return response.text(); } + private async fetchAndCompress(url: string): Promise { + const response = await this.apiService.nativeFetch(new Request(url)); + if (!response.ok) { + throw new Error("Fetch failed"); + } - private async fetchPhishingWebAddresses(url: string) { + const downloadStream = response.body!; + // Pipe through CompressionStream while it's downloading + const compressedStream = downloadStream.pipeThrough(new CompressionStream("gzip")); + // Convert to ArrayBuffer + const buffer = await new Response(compressedStream).arrayBuffer(); + const bytes = new Uint8Array(buffer); + + // Return as Base64 for storage + return (bytes as any).toBase64 ? (bytes as any).toBase64() : this._uint8ToBase64Fallback(bytes); + } + + private async fetchText(url: string) { const response = await this.apiService.nativeFetch(new Request(url)); if (!response.ok) { @@ -225,4 +243,136 @@ export class PhishingDataService { } return []; } + + // Runs the update flow in the background and retries up to 3 times on failure + private async _backgroundUpdate(previous: PhishingDataMeta | null): Promise { + this.logService.info(`[PhishingDataService] Update web addresses triggered...`); + const phishingMeta: PhishingDataMeta = previous ?? { + timestamp: 0, + checksum: "", + applicationVersion: "", + }; + // Start time for logging performance of update + const startTime = Date.now(); + const maxAttempts = 3; + const delayMs = 5 * 60 * 1000; // 5 minutes + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const next = await this.getNextWebAddresses(phishingMeta); + if (!next) { + return; // No update needed + } + + if (next.meta) { + await this._phishingMetaState.update(() => next!.meta!); + } + if (next.blob) { + await this._phishingBlobState.update(() => next!.blob!); + await this._loadBlobToMemory(); + } + + // Performance logging + const elapsed = Date.now() - startTime; + this.logService.info(`[PhishingDataService] Phishing data cache updated in ${elapsed}ms`); + } catch (err) { + this.logService.error( + `[PhishingDataService] Unable to update web addresses. Attempt ${attempt}.`, + err, + ); + if (attempt < maxAttempts) { + await new Promise((res) => setTimeout(res, delayMs)); + } else { + const elapsed = Date.now() - startTime; + this.logService.error( + `[PhishingDataService] Retries unsuccessful after ${elapsed}ms. Unable to update web addresses.`, + err, + ); + } + } + } + } + + // [FIXME] Move compression helpers to a shared utils library + // to separate from phishing data service. + // ------------------------- Blob and Compression Handling ------------------------- + private async _compressString(input: string): Promise { + try { + const stream = new Blob([input]).stream().pipeThrough(new CompressionStream("gzip")); + + const compressedBuffer = await new Response(stream).arrayBuffer(); + const bytes = new Uint8Array(compressedBuffer); + + // Modern browsers support direct toBase64 conversion + // For older support, use fallback + return (bytes as any).toBase64 + ? (bytes as any).toBase64() + : this._uint8ToBase64Fallback(bytes); + } catch (err) { + this.logService.error("[PhishingDataService] Compression failed", err); + return btoa(encodeURIComponent(input)); + } + } + + private async _decompressString(base64: string): Promise { + try { + // Modern browsers support direct toBase64 conversion + // For older support, use fallback + const bytes = (Uint8Array as any).fromBase64 + ? (Uint8Array as any).fromBase64(base64) + : this._base64ToUint8Fallback(base64); + if (bytes == null) { + throw new Error("Base64 decoding resulted in null"); + } + const byteResponse = new Response(bytes); + if (!byteResponse.body) { + throw new Error("Response body is null"); + } + const stream = byteResponse.body.pipeThrough(new DecompressionStream("gzip")); + const streamResponse = new Response(stream); + return await streamResponse.text(); + } catch (err) { + this.logService.error("[PhishingDataService] Decompression failed", err); + return decodeURIComponent(atob(base64)); + } + } + + // Try to load compressed newline blob into an in-memory Set for fast lookups + private async _loadBlobToMemory(): Promise { + this.logService.debug("[PhishingDataService] Loading data blob into memory..."); + try { + const blobBase64 = await firstValueFrom(this._phishingBlobState.state$); + if (!blobBase64) { + return; + } + + const text = await this._decompressString(blobBase64); + // Split and filter + const lines = text.split(/\r?\n/); + const newWebAddressesSet = new Set(lines); + + // Add test addresses + this._testWebAddresses.forEach((a) => newWebAddressesSet.add(a)); + this._webAddressesSet = new Set(newWebAddressesSet); + this.logService.info( + `[PhishingDataService] loaded ${this._webAddressesSet.size} addresses into memory from blob`, + ); + } catch (err) { + this.logService.error("[PhishingDataService] Failed to load blob into memory", err); + } + } + private _uint8ToBase64Fallback(bytes: Uint8Array): string { + const CHUNK_SIZE = 0x8000; // 32KB chunks + let binary = ""; + for (let i = 0; i < bytes.length; i += CHUNK_SIZE) { + const chunk = bytes.subarray(i, i + CHUNK_SIZE); + binary += String.fromCharCode.apply(null, chunk as any); + } + return btoa(binary); + } + + private _base64ToUint8Fallback(base64: string): Uint8Array { + const binary = atob(base64); + return Uint8Array.from(binary, (c) => c.charCodeAt(0)); + } } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index 8004559f57c..c223e4c4925 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -13,8 +13,7 @@ import { mockPorts } from "../../../spec/mock-port.spec-util"; import { BackgroundMemoryStorageService } from "./background-memory-storage.service"; import { ForegroundMemoryStorageService } from "./foreground-memory-storage.service"; -// These are succeeding individually but failing in a batch run - skipping for now -describe.skip("foreground background memory storage interaction", () => { +describe("foreground background memory storage interaction", () => { let foreground: ForegroundMemoryStorageService; let background: BackgroundMemoryStorageService; let logService: MockProxy; @@ -28,7 +27,7 @@ describe.skip("foreground background memory storage interaction", () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); test.each(["has", "get"])( diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 12e1288e806..1f1d4d25b40 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -42,6 +42,7 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; +import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, @@ -85,11 +86,13 @@ import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/v import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { + atRiskPasswordAuthGuard, canAccessAtRiskPasswords, hasAtRiskPasswords, } from "../vault/popup/guards/at-risk-passwords.guard"; import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard"; import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard"; +import { AdminSettingsComponent } from "../vault/popup/settings/admin-settings.component"; import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; import { ArchiveComponent } from "../vault/popup/settings/archive.component"; import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component"; @@ -332,6 +335,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, + { + path: "admin", + component: AdminSettingsComponent, + canActivate: [authGuard, canAccessAutoConfirmSettings], + data: { elevation: 1 } satisfies RouteDataProperties, + }, { path: "clone-cipher", component: AddEditV2Component, @@ -715,7 +724,7 @@ const routes: Routes = [ { path: "at-risk-passwords", component: AtRiskPasswordsComponent, - canActivate: [authGuard, canAccessAtRiskPasswords, hasAtRiskPasswords], + canActivate: [atRiskPasswordAuthGuard, canAccessAtRiskPasswords, hasAtRiskPasswords], }, { path: AuthExtensionRoute.AccountSwitcher, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 8f00569b720..e4cb8a654c4 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -14,16 +14,11 @@ import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { catchError, concatMap, - distinctUntilChanged, filter, firstValueFrom, map, of, - pairwise, - startWith, Subject, - switchMap, - take, takeUntil, tap, } from "rxjs"; @@ -38,7 +33,7 @@ import { } from "@bitwarden/auth/common"; import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -83,7 +78,8 @@ export class AppComponent implements OnInit, OnDestroy { private lastActivity: Date; private activeUserId: UserId; private routerAnimations = false; - private processingPendingAuth = false; + private processingPendingAuthRequests = false; + private shouldRerunAuthRequestProcessing = false; private destroy$ = new Subject(); @@ -118,7 +114,7 @@ export class AppComponent implements OnInit, OnDestroy { private logService: LogService, private authRequestService: AuthRequestServiceAbstraction, private pendingAuthRequestsState: PendingAuthRequestsStateService, - private authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction, + private authRequestAnsweringService: AuthRequestAnsweringService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -136,22 +132,7 @@ export class AppComponent implements OnInit, OnDestroy { this.activeUserId = account?.id; }); - // Trigger processing auth requests when the active user is in an unlocked state. Runs once when - // the popup is open. - this.accountService.activeAccount$ - .pipe( - map((a) => a?.id), // Extract active userId - distinctUntilChanged(), // Only when userId actually changes - filter((userId) => userId != null), // Require a valid userId - switchMap((userId) => this.authService.authStatusFor$(userId).pipe(take(1))), // Get current auth status once for new user - filter((status) => status === AuthenticationStatus.Unlocked), // Only when the new user is Unlocked - tap(() => { - // Trigger processing when switching users while popup is open - void this.authRequestAnsweringService.processPendingAuthRequests(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); + this.authRequestAnsweringService.setupUnlockListenersForProcessingAuthRequests(this.destroy$); this.authService.activeAccountStatus$ .pipe( @@ -163,23 +144,6 @@ export class AppComponent implements OnInit, OnDestroy { ) .subscribe(); - // When the popup is already open and the active account transitions to Unlocked, - // process any pending auth requests for the active user. The above subscription does not handle - // this case. - this.authService.activeAccountStatus$ - .pipe( - startWith(null as unknown as AuthenticationStatus), // Seed previous value to handle initial emission - pairwise(), // Compare previous and current statuses - filter( - ([prev, curr]) => - prev !== AuthenticationStatus.Unlocked && curr === AuthenticationStatus.Unlocked, // Fire on transitions into Unlocked (incl. initial) - ), - takeUntil(this.destroy$), - ) - .subscribe(() => { - void this.authRequestAnsweringService.processPendingAuthRequests(); - }); - this.ngZone.runOutsideAngular(() => { window.onmousedown = () => this.recordActivity(); window.ontouchstart = () => this.recordActivity(); @@ -234,38 +198,31 @@ export class AppComponent implements OnInit, OnDestroy { await this.router.navigate(["lock"]); } else if (msg.command === "openLoginApproval") { - if (this.processingPendingAuth) { + if (this.processingPendingAuthRequests) { + // If an "openLoginApproval" message is received while we are currently processing other + // auth requests, then set a flag so we remember to process that new auth request + this.shouldRerunAuthRequestProcessing = true; return; } - this.processingPendingAuth = true; - try { - // Always query server for all pending requests and open a dialog for each - const pendingList = await firstValueFrom( - this.authRequestService.getPendingAuthRequests$(), - ); - if (Array.isArray(pendingList) && pendingList.length > 0) { - const respondedIds = new Set(); - for (const req of pendingList) { - if (req?.id == null) { - continue; - } - const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { - notificationId: req.id, - }); - const result = await firstValueFrom(dialogRef.closed); + /** + * This do/while loop allows us to: + * - a) call processPendingAuthRequests() once on "openLoginApproval" + * - b) remember to re-call processPendingAuthRequests() if another "openLoginApproval" was + * received while we were processing the original auth requests + */ + do { + this.shouldRerunAuthRequestProcessing = false; - if (result !== undefined && typeof result === "boolean") { - respondedIds.add(req.id); - if (respondedIds.size === pendingList.length && this.activeUserId != null) { - await this.pendingAuthRequestsState.clear(this.activeUserId); - } - } - } + try { + await this.processPendingAuthRequests(); + } catch (error) { + this.logService.error(`Error processing pending auth requests: ${error}`); + this.shouldRerunAuthRequestProcessing = false; // Reset flag to prevent infinite loop on persistent errors } - } finally { - this.processingPendingAuth = false; - } + // If an "openLoginApproval" message was received while processPendingAuthRequests() was running, then + // shouldRerunAuthRequestProcessing will have been set to true + } while (this.shouldRerunAuthRequestProcessing); } else if (msg.command === "showDialog") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -403,4 +360,39 @@ export class AppComponent implements OnInit, OnDestroy { this.toastService.showToast(toastOptions); } + + private async processPendingAuthRequests() { + this.processingPendingAuthRequests = true; + + try { + // Always query server for all pending requests and open a dialog for each + const pendingList = await firstValueFrom(this.authRequestService.getPendingAuthRequests$()); + + if (Array.isArray(pendingList) && pendingList.length > 0) { + const respondedIds = new Set(); + + for (const req of pendingList) { + if (req?.id == null) { + continue; + } + + const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { + notificationId: req.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + respondedIds.add(req.id); + + if (respondedIds.size === pendingList.length && this.activeUserId != null) { + await this.pendingAuthRequestsState.clear(this.activeUserId); + } + } + } + } + } finally { + this.processingPendingAuthRequests = false; + } + } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 63d761fac2e..ba1896b1dbb 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -3,7 +3,11 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { merge, of, Subject } from "rxjs"; -import { CollectionService } from "@bitwarden/admin-console/common"; +import { + CollectionService, + OrganizationUserApiService, + OrganizationUserService, +} 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"; @@ -40,22 +44,29 @@ import { LogoutService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} 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 { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + InternalOrganizationServiceAbstraction, + 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, AccountService as AccountServiceAbstraction, } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AutofillSettingsService, @@ -498,18 +509,19 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: AuthRequestAnsweringServiceAbstraction, - useClass: AuthRequestAnsweringService, + provide: AuthRequestAnsweringService, + useClass: ExtensionAuthRequestAnsweringService, deps: [ AccountServiceAbstraction, - ActionsService, AuthService, - I18nServiceAbstraction, MasterPasswordServiceAbstraction, MessagingService, PendingAuthRequestsStateService, + ActionsService, + I18nServiceAbstraction, PlatformUtilsService, SystemNotificationsService, + LogService, ], }), safeProvider({ @@ -529,6 +541,7 @@ const safeProviders: SafeProvider[] = [ AccountService, BillingAccountProfileStateService, ConfigService, + LogService, OrganizationService, PlatformUtilsService, StateProvider, @@ -748,6 +761,19 @@ 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/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 8f30d00cc31..f180564b912 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -9,9 +9,9 @@ import { map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SendId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 1a3df238543..521d72bba0c 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -11,9 +11,9 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; diff --git a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts index 1f0d9f2a0c9..ddf50eb39bf 100644 --- a/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-file-popout-dialog/send-file-popout-dialog-container.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common"; import { Component, input, OnInit } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { CenterPositionStrategy, DialogService } from "@bitwarden/components"; import { SendFormConfig } from "@bitwarden/send-ui"; diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 6e73d9811f2..dfbfabf8d5e 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -17,10 +17,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { ButtonModule, NoItemsModule } from "@bitwarden/components"; import { diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index 89769bdd1ce..f36a475a805 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -13,7 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv 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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { skeletonLoadingDelay } from "@bitwarden/common/vault/utils/skeleton-loading.operator"; @@ -139,7 +139,7 @@ export class SendV2Component implements OnDestroy { .pipe(takeUntilDestroyed()) .subscribe(([emptyList, noFilteredResults, currentFilter]) => { if (currentFilter?.sendType !== null) { - this.title = this.sendTypeTitles[currentFilter.sendType] ?? "allSends"; + this.title = this.sendTypeTitles[currentFilter.sendType as SendType] ?? "allSends"; } else { this.title = "allSends"; } diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index 06c89e15f59..c6f1c9dbc3b 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -82,6 +82,24 @@ + + @if (showAdminSettingsLink$ | async) { + + + +
+

{{ "admin" | i18n }}

+ @if (showAdminBadge$ | async) { + 1 + } +
+ +
+
+ } + diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.spec.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.spec.ts index 4cc3ed0149c..a05fa45753e 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.spec.ts @@ -6,6 +6,7 @@ import { BehaviorSubject, firstValueFrom, of, Subject } from "rxjs"; import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { AutofillBrowserSettingsService } from "@bitwarden/browser/autofill/services/autofill-browser-settings.service"; import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -42,6 +43,9 @@ describe("SettingsV2Component", () => { defaultBrowserAutofillDisabled$: Subject; isBrowserAutofillSettingOverridden: jest.Mock>; }; + let mockAutoConfirmService: { + canManageAutoConfirm$: jest.Mock; + }; let dialogService: MockProxy; let openSpy: jest.SpyInstance; @@ -66,6 +70,10 @@ describe("SettingsV2Component", () => { isBrowserAutofillSettingOverridden: jest.fn().mockResolvedValue(false), }; + mockAutoConfirmService = { + canManageAutoConfirm$: jest.fn().mockReturnValue(of(false)), + }; + jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue("Chrome"); const cfg = TestBed.configureTestingModule({ @@ -75,6 +83,7 @@ describe("SettingsV2Component", () => { { provide: BillingAccountProfileStateService, useValue: mockBillingState }, { provide: NudgesService, useValue: mockNudges }, { provide: AutofillBrowserSettingsService, useValue: mockAutofillSettings }, + { provide: AutomaticUserConfirmationService, useValue: mockAutoConfirmService }, { provide: DialogService, useValue: dialogService }, { provide: I18nService, useValue: { t: jest.fn((key: string) => key) } }, { provide: GlobalStateProvider, useValue: new FakeGlobalStateProvider() }, diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts index e10d41b9445..2c9f893c99c 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts @@ -7,7 +7,9 @@ import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/compon import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; 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"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -65,13 +67,25 @@ export class SettingsV2Component { ), ); + showAdminBadge$: Observable = this.authenticatedAccount$.pipe( + switchMap((account) => + this.nudgesService.showNudgeBadge$(NudgeType.AutoConfirmNudge, account.id), + ), + ); + showAutofillBadge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.AutofillNudge, account.id)), ); + showAdminSettingsLink$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.autoConfirmService.canManageAutoConfirm$(userId)), + ); + constructor( private readonly nudgesService: NudgesService, private readonly accountService: AccountService, + private readonly autoConfirmService: AutomaticUserConfirmationService, private readonly accountProfileStateService: BillingAccountProfileStateService, private readonly dialogService: DialogService, ) {} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 8f184c6a0c1..8b4d2d21b8b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -1,3 +1,5 @@ +@let previouslyCouldArchive = !(userCanArchive$ | async) && config?.originalCipher?.archivedDate; + + @if (config?.originalCipher?.archivedDate) { + + + {{ "archived" | i18n }} + + + } @@ -24,21 +33,41 @@ - + + @if (isEditMode) { + @if ((archiveFlagEnabled$ | async) && isCipherArchived) { + + } + @if ((userCanArchive$ | async) && canCipherBeArchived) { + + } + } + @if (canDeleteCipher$ | async) { + + } + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index f2c9d470816..8ea23e7e2b9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,10 +1,15 @@ +import { Location } from "@angular/common"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; +import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,13 +17,17 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { TaskService } from "@bitwarden/common/vault/tasks"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; +import { DialogService } from "@bitwarden/components"; import { + ArchiveCipherUtilitiesService, CipherFormConfig, CipherFormConfigService, CipherFormMode, @@ -45,15 +54,17 @@ describe("AddEditV2Component", () => { let cipherServiceMock: MockProxy; const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; - const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ ...buildConfigResponse, mode }), - ); + const buildConfig = jest.fn((mode) => Promise.resolve({ ...buildConfigResponse, mode })); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); const navigate = jest.fn(); const back = jest.fn().mockResolvedValue(null); const setHistory = jest.fn(); const collect = jest.fn().mockResolvedValue(null); + const history$ = jest.fn(); + const historyGo = jest.fn().mockResolvedValue(null); + const openSimpleDialog = jest.fn().mockResolvedValue(true); + const cipherArchiveService = mock(); beforeEach(async () => { buildConfig.mockClear(); @@ -61,6 +72,12 @@ describe("AddEditV2Component", () => { navigate.mockClear(); back.mockClear(); collect.mockClear(); + history$.mockClear(); + historyGo.mockClear(); + openSimpleDialog.mockClear(); + + cipherArchiveService.hasArchiveFlagEnabled$ = of(true); + cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock({ @@ -70,11 +87,13 @@ describe("AddEditV2Component", () => { await TestBed.configureTestingModule({ imports: [AddEditV2Component], providers: [ + provideNoopAnimations(), { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, - { provide: PopupRouterCacheService, useValue: { back, setHistory } }, + { provide: PopupRouterCacheService, useValue: { back, setHistory, history$ } }, { provide: PopupCloseWarningService, useValue: { disable } }, { provide: Router, useValue: { navigate } }, + { provide: Location, useValue: { historyGo } }, { provide: ActivatedRoute, useValue: { queryParams: queryParams$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, @@ -83,10 +102,33 @@ describe("AddEditV2Component", () => { { provide: CipherAuthorizationService, useValue: { - canDeleteCipher$: jest.fn().mockReturnValue(true), + canDeleteCipher$: jest.fn().mockReturnValue(of(true)), }, }, { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, + { + provide: TaskService, + useValue: mock(), + }, + { + provide: ViewCacheService, + useValue: { signal: jest.fn(() => (): any => null) }, + }, + { + provide: BillingAccountProfileStateService, + useValue: mock(), + }, + { + provide: CipherArchiveService, + useValue: cipherArchiveService, + }, + { + provide: ArchiveCipherUtilitiesService, + useValue: { + archiveCipher: jest.fn().mockResolvedValue(null), + unarchiveCipher: jest.fn().mockResolvedValue(null), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -94,6 +136,11 @@ describe("AddEditV2Component", () => { buildConfig, }, }) + .overrideProvider(DialogService, { + useValue: { + openSimpleDialog, + }, + }) .compileComponents(); fixture = TestBed.createComponent(AddEditV2Component); @@ -356,6 +403,151 @@ describe("AddEditV2Component", () => { }); }); + describe("submit button text", () => { + beforeEach(() => { + // prevent form from rendering + jest.spyOn(component as any, "loading", "get").mockReturnValue(true); + }); + + it("sets it to 'save' by default", fakeAsync(() => { + buildConfigResponse.originalCipher = {} as Cipher; + + queryParams$.next({}); + + tick(); + + const submitBtn = fixture.debugElement.query(By.css("button[type=submit]")); + expect(submitBtn.nativeElement.textContent.trim()).toBe("save"); + })); + + it("sets it to 'save' when the user is able to archive the item", fakeAsync(() => { + buildConfigResponse.originalCipher = { isArchived: false } as any; + + queryParams$.next({}); + + tick(); + + const submitBtn = fixture.debugElement.query(By.css("button[type=submit]")); + expect(submitBtn.nativeElement.textContent.trim()).toBe("save"); + })); + + it("sets it to 'unarchiveAndSave' when the user cannot archive and the item is archived", fakeAsync(() => { + cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); + buildConfigResponse.originalCipher = { isArchived: true } as any; + + queryParams$.next({}); + tick(); + + const submitBtn = fixture.debugElement.query(By.css("button[type=submit]")); + expect(submitBtn.nativeElement.textContent.trim()).toBe("save"); + })); + }); + + describe("archive", () => { + it("calls archiveCipherUtilsService service to archive the cipher", async () => { + buildConfigResponse.originalCipher = { id: "222-333-444-5555", edit: true } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + await fixture.whenStable(); + await component.archive(); + + expect(component["archiveCipherUtilsService"].archiveCipher).toHaveBeenCalledWith( + expect.objectContaining({ id: "222-333-444-5555" }), + true, + ); + }); + }); + + describe("unarchive", () => { + it("calls archiveCipherUtilsService service to unarchive the cipher", async () => { + buildConfigResponse.originalCipher = { + id: "222-333-444-5555", + archivedDate: new Date(), + edit: true, + } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + await component.unarchive(); + + expect(component["archiveCipherUtilsService"].unarchiveCipher).toHaveBeenCalledWith( + expect.objectContaining({ id: "222-333-444-5555" }), + ); + }); + }); + + describe("archive button", () => { + beforeEach(() => { + // prevent form from rendering + jest.spyOn(component as any, "loading", "get").mockReturnValue(true); + buildConfigResponse.originalCipher = { archivedDate: undefined, edit: true } as Cipher; + }); + + it("shows the archive button when the user can archive and the cipher can be archived", fakeAsync(() => { + cipherArchiveService.userCanArchive$.mockReturnValue(of(true)); + queryParams$.next({ cipherId: "222-333-444-5555" }); + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeTruthy(); + })); + + it("does not show the archive button when the user cannot archive", fakeAsync(() => { + cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeFalsy(); + })); + + it("does not show the archive button when the cipher cannot be archived", fakeAsync(() => { + cipherArchiveService.userCanArchive$.mockReturnValue(of(true)); + buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeFalsy(); + })); + }); + + describe("unarchive button", () => { + beforeEach(() => { + // prevent form from rendering + jest.spyOn(component as any, "loading", "get").mockReturnValue(true); + buildConfigResponse.originalCipher = { edit: true } as Cipher; + }); + + it("shows the unarchive button when the cipher is archived", fakeAsync(() => { + buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher; + + tick(); + fixture.detectChanges(); + + const unarchiveBtn = fixture.debugElement.query( + By.css("button[biticonbutton='bwi-unarchive']"), + ); + expect(unarchiveBtn).toBeTruthy(); + })); + + it("does not show the unarchive button when the cipher is not archived", fakeAsync(() => { + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + fixture.detectChanges(); + + const unarchiveBtn = fixture.debugElement.query( + By.css("button[biticonbutton='bwi-unarchive']"), + ); + expect(unarchiveBtn).toBeFalsy(); + })); + }); + describe("delete", () => { it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { const dialogSpy = jest @@ -374,12 +566,104 @@ describe("AddEditV2Component", () => { expect(deleteCipherSpy).toHaveBeenCalled(); }); - it("navigates to vault tab after deletion", async () => { + it("navigates to vault tab after deletion by default", async () => { jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); await component.delete(); expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); }); + + it("navigates to custom route when not in history", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: true, id: "123" } as Cipher; + queryParams$.next({ + cipherId: "123", + routeAfterDeletion: "/archive", + }); + + tick(); + + // Mock history without the target route + history$.mockReturnValue( + of([ + { url: "/tabs/vault" }, + { url: "/view-cipher?cipherId=123" }, + { url: "/add-edit?cipherId=123" }, + ]), + ); + + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + void component.delete(); + tick(); + + expect(history$).toHaveBeenCalled(); + expect(historyGo).not.toHaveBeenCalled(); + expect(navigate).toHaveBeenCalledWith(["/archive"]); + })); + + it("uses historyGo when custom route exists in history", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: true, id: "123" } as Cipher; + queryParams$.next({ + cipherId: "123", + routeAfterDeletion: "/archive", + }); + + tick(); + + history$.mockReturnValue( + of([ + { url: "/tabs/vault" }, + { url: "/archive" }, + { url: "/view-cipher?cipherId=123" }, + { url: "/add-edit?cipherId=123" }, + ]), + ); + + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + void component.delete(); + tick(); + + expect(history$).toHaveBeenCalled(); + expect(historyGo).toHaveBeenCalledWith(-2); + expect(navigate).not.toHaveBeenCalled(); + })); + + it("uses router.navigate for default /tabs/vault route", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: true, id: "456" } as Cipher; + component.routeAfterDeletion = "/tabs/vault"; + + queryParams$.next({ + cipherId: "456", + }); + + tick(); + + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + void component.delete(); + tick(); + + expect(history$).not.toHaveBeenCalled(); + expect(historyGo).not.toHaveBeenCalled(); + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + })); + + it("ignores invalid routeAfterDeletion query param and uses default route", fakeAsync(() => { + // Reset the component's routeAfterDeletion to default before this test + component.routeAfterDeletion = "/tabs/vault"; + + buildConfigResponse.originalCipher = { edit: true, id: "456" } as Cipher; + queryParams$.next({ + cipherId: "456", + routeAfterDeletion: "/invalid/route", + }); + + tick(); + + // The invalid route should be ignored, routeAfterDeletion should remain default + expect(component.routeAfterDeletion).toBe("/tabs/vault"); + })); }); describe("reloadAddEditCipherData", () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 22aad854dd0..895a5fe0cce 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, OnInit, OnDestroy } from "@angular/core"; +import { CommonModule, Location } from "@angular/common"; +import { Component, OnInit, OnDestroy, viewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; @@ -16,6 +16,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; 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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { CipherType, toCipherType } from "@bitwarden/common/vault/enums"; @@ -29,8 +30,11 @@ import { IconButtonModule, DialogService, ToastService, + BadgeModule, } from "@bitwarden/components"; import { + ArchiveCipherUtilitiesService, + CipherFormComponent, CipherFormConfig, CipherFormConfigService, CipherFormGenerationService, @@ -60,6 +64,18 @@ import { import { VaultPopoutType } from "../../../utils/vault-popout-window"; import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component"; +/** + * Available routes to navigate to after editing 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]; + /** * Helper class to parse query parameters for the AddEdit route. */ @@ -75,6 +91,7 @@ class QueryParams { this.username = params.username; this.name = params.name; this.prefillNameAndURIFromTab = params.prefillNameAndURIFromTab; + this.routeAfterDeletion = params.routeAfterDeletion ?? ROUTES_AFTER_EDIT_DELETION.tabsVault; } /** @@ -127,6 +144,12 @@ class QueryParams { * NOTE: This will override the `uri` and `name` query parameters if set to true. */ prefillNameAndURIFromTab?: true; + + /** + * The view that will be navigated to after deleting the cipher. + * @default "/tabs/vault" + */ + routeAfterDeletion?: ROUTES_AFTER_EDIT_DELETION; } export type AddEditQueryParams = Partial>; @@ -156,12 +179,15 @@ export type AddEditQueryParams = Partial>; AsyncActionsModule, PopOutComponent, IconButtonModule, + BadgeModule, ], }) export class AddEditV2Component implements OnInit, OnDestroy { + readonly cipherFormComponent = viewChild(CipherFormComponent); headerText: string; config: CipherFormConfig; canDeleteCipher$: Observable; + routeAfterDeletion: ROUTES_AFTER_EDIT_DELETION = "/tabs/vault"; get loading() { return this.config == null; @@ -171,9 +197,26 @@ export class AddEditV2Component implements OnInit, OnDestroy { return this.config?.originalCipher?.id as CipherId; } + get cipher(): CipherView { + return new CipherView(this.config?.originalCipher); + } + + get canCipherBeArchived(): boolean { + return this.cipher?.canBeArchived; + } + + get isCipherArchived(): boolean { + return this.cipher?.isArchived; + } + private fido2PopoutSessionData$ = fido2PopoutSessionData$(); private fido2PopoutSessionData: Fido2SessionData; + protected userId$ = this.accountService.activeAccount$.pipe(getUserId); + protected userCanArchive$ = this.userId$.pipe( + switchMap((userId) => this.archiveService.userCanArchive$(userId)), + ); + private get inFido2PopoutWindow() { return BrowserPopupUtils.inPopout(window) && this.fido2PopoutSessionData.isFido2Session; } @@ -182,6 +225,8 @@ export class AddEditV2Component implements OnInit, OnDestroy { return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem); } + protected archiveFlagEnabled$ = this.archiveService.hasArchiveFlagEnabled$; + constructor( private route: ActivatedRoute, private i18nService: I18nService, @@ -196,6 +241,9 @@ export class AddEditV2Component implements OnInit, OnDestroy { private dialogService: DialogService, protected cipherAuthorizationService: CipherAuthorizationService, private accountService: AccountService, + private location: Location, + private archiveService: CipherArchiveService, + private archiveCipherUtilsService: ArchiveCipherUtilitiesService, ) { this.subscribeToParams(); } @@ -322,6 +370,10 @@ export class AddEditV2Component implements OnInit, OnDestroy { await BrowserApi.sendMessage("addEditCipherSubmitted"); } + get isEditMode(): boolean { + return ["edit", "partial-edit"].includes(this.config?.mode); + } + subscribeToParams(): void { this.route.queryParams .pipe( @@ -345,9 +397,7 @@ export class AddEditV2Component implements OnInit, OnDestroy { } config.initialValues = await this.setInitialValuesFromParams(params); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(getUserId), - ); + const activeUserId = await firstValueFrom(this.userId$); // The browser notification bar and overlay use addEditCipherInfo$ to pass modified cipher details to the form // Attempt to fetch them here and overwrite the initialValues if present @@ -378,6 +428,13 @@ export class AddEditV2Component implements OnInit, OnDestroy { ); } + if ( + params.routeAfterDeletion && + Object.values(ROUTES_AFTER_EDIT_DELETION).includes(params.routeAfterDeletion) + ) { + this.routeAfterDeletion = params.routeAfterDeletion; + } + return config; }), ) @@ -430,6 +487,40 @@ export class AddEditV2Component implements OnInit, OnDestroy { return this.i18nService.t(translation[type]); } + /** + * Update the cipher in the form after archiving/unarchiving. + * @param revisionDate The new revision date. + * @param archivedDate The new archived date (null if unarchived). + **/ + updateCipherFromArchive = (revisionDate: Date, archivedDate: Date | null) => { + this.cipherFormComponent().patchCipher((current) => { + current.revisionDate = revisionDate; + current.archivedDate = archivedDate; + return current; + }); + }; + + archive = async () => { + const cipherResponse = await this.archiveCipherUtilsService.archiveCipher(this.cipher, true); + + if (!cipherResponse) { + return; + } + this.updateCipherFromArchive( + new Date(cipherResponse.revisionDate), + new Date(cipherResponse.archivedDate), + ); + }; + + unarchive = async () => { + const cipherResponse = await this.archiveCipherUtilsService.unarchiveCipher(this.cipher); + + if (!cipherResponse) { + return; + } + this.updateCipherFromArchive(new Date(cipherResponse.revisionDate), null); + }; + delete = async () => { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "deleteItem" }, @@ -444,14 +535,28 @@ export class AddEditV2Component implements OnInit, OnDestroy { } try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const activeUserId = await firstValueFrom(this.userId$); await this.deleteCipher(activeUserId); } catch (e) { this.logService.error(e); return false; } - await this.router.navigate(["/tabs/vault"]); + if (this.routeAfterDeletion !== ROUTES_AFTER_EDIT_DELETION.tabsVault) { + const history = await firstValueFrom(this.popupRouterCacheService.history$()); + const targetIndex = history.map((h) => h.url).lastIndexOf(this.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 + await this.location.historyGo(stepsBack); + } else { + await this.router.navigate([this.routeAfterDeletion]); + } + } else { + await this.router.navigate([this.routeAfterDeletion]); + } this.toastService.showToast({ variant: "success", diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index b86ec24fd20..be67869d3df 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -3,62 +3,63 @@ type="button" bitIconButton="bwi-ellipsis-v" size="small" - [label]="'moreOptionsLabel' | i18n: cipher.name" - [disabled]="decryptionFailure" + [label]="'moreOptionsLabelNoPlaceholder' | i18n" [bitMenuTriggerFor]="moreOptions" > - - - + + + + - - - - - - @if (canEdit) { - - } - - - {{ "clone" | i18n }} - - - {{ "assignToCollections" | i18n }} - - - @if (showArchive$ | async) { - @if (canArchive$ | async) { - - } @else { - + } @else { + + + } } } @if (canDelete$ | async) { diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts index bd9ce108522..b999d8db35a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts @@ -158,14 +158,6 @@ describe("ItemMoreOptionsComponent", () => { expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); }); - it("does not show the exact match dialog when the default match strategy is Exact and autofill confirmation is not to be shown", async () => { - uriMatchStrategy$.next(UriMatchStrategy.Exact); - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); - }); - describe("autofill confirmation dialog", () => { beforeEach(() => { uriMatchStrategy$.next(UriMatchStrategy.Domain); @@ -236,22 +228,30 @@ describe("ItemMoreOptionsComponent", () => { }); describe("URI match strategy handling", () => { + it("calls the passwordService to passwordRepromptCheck", async () => { + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); + + await component.doAutofill(); + + expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher); + }); + describe("when the default URI match strategy is Exact", () => { beforeEach(() => { uriMatchStrategy$.next(UriMatchStrategy.Exact); }); - it("calls the passwordService to passwordRepromptCheck", async () => { - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); - mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); - - await component.doAutofill(); - - expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher); - }); - - it("shows the exact match dialog", async () => { + it("shows the exact match dialog when the cipher has no saved URIs", async () => { autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [], + }, + })); await component.doAutofill(); @@ -266,6 +266,53 @@ describe("ItemMoreOptionsComponent", () => { expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); }); + + it("does not show the exact match dialog when the cipher has at least one non-exact match uri", async () => { + mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly); + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com", match: UriMatchStrategy.Domain }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows the exact match dialog when the cipher uris all have a match strategy of Exact", async () => { + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com/a", match: UriMatchStrategy.Exact }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" }); + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.objectContaining({ key: "cannotAutofill" }), + content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), + type: "info", + }), + ); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); }); describe("when the default URI match strategy is not Exact", () => { @@ -273,7 +320,45 @@ describe("ItemMoreOptionsComponent", () => { mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); uriMatchStrategy$.next(UriMatchStrategy.Domain); }); - it("does not show the exact match dialog", async () => { + + it("does not show the exact match dialog when the cipher has no saved URIs", async () => { + autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); + + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + }); + + it("shows the exact match dialog when the cipher has only exact match saved URIs", async () => { + cipherService.getFullCipherView.mockImplementation(async (c) => ({ + ...baseCipher, + ...c, + login: { + ...baseCipher.login, + uris: [ + { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, + { uri: "https://two.example.com/a", match: UriMatchStrategy.Exact }, + ], + }, + })); + + autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); + + await component.doAutofill(); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.objectContaining({ key: "cannotAutofill" }), + content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), + type: "info", + }), + ); + expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); + expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); + }); + + it("does not show the exact match dialog when the cipher has at least one uri without a match strategy of Exact", async () => { + mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); cipherService.getFullCipherView.mockImplementation(async (c) => ({ ...baseCipher, ...c, @@ -292,70 +377,6 @@ describe("ItemMoreOptionsComponent", () => { expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); }); - - it("shows the exact match dialog when the cipher has a single uri with a match strategy of Exact", async () => { - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [{ uri: "https://one.example.com", match: UriMatchStrategy.Exact }], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( - expect.objectContaining({ - title: expect.objectContaining({ key: "cannotAutofill" }), - content: expect.objectContaining({ key: "cannotAutofillExactMatch" }), - type: "info", - }), - ); - expect(autofillSvc.doAutofill).not.toHaveBeenCalled(); - expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled(); - }); - }); - - it("does not show the exact match dialog when the cipher has no uris", async () => { - mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); - }); - - it("does not show the exact match dialog when the cipher has a uri with a match strategy of Exact and a uri with a match strategy of Domain", async () => { - mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled); - cipherService.getFullCipherView.mockImplementation(async (c) => ({ - ...baseCipher, - ...c, - login: { - ...baseCipher.login, - uris: [ - { uri: "https://one.example.com", match: UriMatchStrategy.Exact }, - { uri: "https://page.example.com", match: UriMatchStrategy.Domain }, - ], - }, - })); - - autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" }); - - await component.doAutofill(); - - expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); }); }); @@ -384,4 +405,42 @@ describe("ItemMoreOptionsComponent", () => { }); }); }); + + describe("canAssignCollections$", () => { + it("emits true when user has organizations and editable collections", (done) => { + jest.spyOn(component["organizationService"], "hasOrganizations").mockReturnValue(of(true)); + jest + .spyOn(component["collectionService"], "decryptedCollections$") + .mockReturnValue(of([{ id: "col-1", readOnly: false }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("emits false when user has no organizations", (done) => { + jest.spyOn(component["organizationService"], "hasOrganizations").mockReturnValue(of(false)); + jest + .spyOn(component["collectionService"], "decryptedCollections$") + .mockReturnValue(of([{ id: "col-1", readOnly: false }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + + it("emits false when all collections are read-only", (done) => { + jest.spyOn(component["organizationService"], "hasOrganizations").mockReturnValue(of(true)); + jest + .spyOn(component["collectionService"], "decryptedCollections$") + .mockReturnValue(of([{ id: "col-1", readOnly: true }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index c4353e17bef..ce797d9755e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -204,12 +204,15 @@ export class ItemMoreOptionsComponent { } const uris = cipher.login?.uris ?? []; - const cipherHasAllExactMatchLoginUris = - uris.length > 0 && uris.every((u) => u.uri && u.match === UriMatchStrategy.Exact); - const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$); - if (cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact) { + 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" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts index 883d17b61c3..e6dffdaff08 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts @@ -10,6 +10,10 @@ import { BehaviorSubject, Observable, Subject, of } from "rxjs"; import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; import { NudgeType, NudgesService } from "@bitwarden/angular/vault"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { + AutoConfirmExtensionSetupDialogComponent, + AutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { CurrentAccountComponent } from "@bitwarden/browser/auth/popup/account-switching/current-account.component"; import AutofillService from "@bitwarden/browser/autofill/services/autofill.service"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; @@ -136,6 +140,7 @@ class VaultListItemsContainerStubComponent { const mockDialogRef = { close: jest.fn(), afterClosed: jest.fn().mockReturnValue(of(undefined)), + closed: of(undefined), } as unknown as import("@bitwarden/components").DialogRef; jest @@ -145,6 +150,11 @@ jest jest .spyOn(DecryptionFailureDialogComponent, "open") .mockImplementation((_: DialogService, _params: any) => mockDialogRef as any); + +const autoConfirmDialogSpy = jest + .spyOn(AutoConfirmExtensionSetupDialogComponent, "open") + .mockImplementation((_: DialogService) => mockDialogRef as any); + jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false); jest.spyOn(BrowserPopupUtils, "openCurrentPagePopout").mockResolvedValue(); @@ -222,6 +232,13 @@ describe("VaultV2Component", () => { getFeatureFlag$: jest.fn().mockImplementation((_flag: string) => of(false)), }; + const autoConfirmSvc = { + configuration$: jest.fn().mockReturnValue(of({})), + canManageAutoConfirm$: jest.fn().mockReturnValue(of(false)), + upsert: jest.fn().mockResolvedValue(undefined), + autoConfirmUser: jest.fn().mockResolvedValue(undefined), + }; + beforeEach(async () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ @@ -275,6 +292,10 @@ describe("VaultV2Component", () => { provide: SearchService, useValue: { isCipherSearching$: of(false) }, }, + { + provide: AutomaticUserConfirmationService, + useValue: autoConfirmSvc, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -588,4 +609,86 @@ describe("VaultV2Component", () => { const spotlights = queryAllSpotlights(fixture); expect(spotlights.length).toBe(0); })); + + describe("AutoConfirmExtensionSetupDialog", () => { + beforeEach(() => { + autoConfirmDialogSpy.mockClear(); + }); + + it("opens dialog when canManage is true and showBrowserNotification is undefined", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: undefined, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).toHaveBeenCalledWith(expect.any(Object)); + })); + + it("does not open dialog when showBrowserNotification is false", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: false, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + + it("does not open dialog when showBrowserNotification is true", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(true)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: true, + showSetupDialog: true, + showBrowserNotification: true, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + + it("does not open dialog when canManage is false even if showBrowserNotification is undefined", fakeAsync(() => { + autoConfirmSvc.canManageAutoConfirm$.mockReturnValue(of(false)); + autoConfirmSvc.configuration$.mockReturnValue( + of({ + enabled: false, + showSetupDialog: true, + showBrowserNotification: undefined, + }), + ); + + const fixture = TestBed.createComponent(VaultV2Component); + const component = fixture.componentInstance; + + void component.ngOnInit(); + tick(); + + expect(autoConfirmDialogSpy).not.toHaveBeenCalled(); + })); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 30d1d21abfb..761b366bcd2 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -15,6 +15,7 @@ import { shareReplay, switchMap, take, + withLatestFrom, tap, BehaviorSubject, } from "rxjs"; @@ -25,6 +26,11 @@ import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { DeactivatedOrg, NoResults, VaultOpen } from "@bitwarden/assets/svg"; +import { + AutoConfirmExtensionSetupDialogComponent, + AutoConfirmState, + AutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; @@ -41,6 +47,7 @@ import { ButtonModule, DialogService, NoItemsModule, + ToastService, TypographyModule, } from "@bitwarden/components"; import { @@ -267,6 +274,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private introCarouselService: IntroCarouselService, private nudgesService: NudgesService, private router: Router, + private autoConfirmService: AutomaticUserConfirmationService, + private toastService: ToastService, private vaultProfileService: VaultProfileService, private billingAccountService: BillingAccountProfileStateService, private liveAnnouncer: LiveAnnouncer, @@ -329,6 +338,36 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { }); }); + const autoConfirmState$ = this.autoConfirmService.configuration$(this.activeUserId); + + combineLatest([ + this.autoConfirmService.canManageAutoConfirm$(this.activeUserId), + autoConfirmState$, + ]) + .pipe( + filter(([canManage, state]) => canManage && state.showBrowserNotification === undefined), + take(1), + switchMap(() => AutoConfirmExtensionSetupDialogComponent.open(this.dialogService).closed), + withLatestFrom(autoConfirmState$, this.accountService.activeAccount$.pipe(getUserId)), + switchMap(([result, state, userId]) => { + const newState: AutoConfirmState = { + ...state, + enabled: result ?? false, + showBrowserNotification: !result, + }; + + if (result) { + this.toastService.showToast({ + message: this.i18nService.t("autoConfirmEnabled"), + variant: "success", + }); + } + + return this.autoConfirmService.upsert(userId, newState); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); await this.vaultItemsTransferService.enforceOrganizationDataOwnership(this.activeUserId); this.readySubject.next(true); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html index 9b8380a4214..03eb701704f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -1,39 +1,56 @@ - + + @if (cipher?.isArchived) { + + {{ "archived" | i18n }} + + } + + - + @if (cipher) { + + } - - - - - + @if (!cipher.isDeleted) { + + } + @if (cipher.isDeleted && cipher.permissions.restore) { + + } + + @if ((archiveFlagEnabled$ | async) && cipher.isArchived) { + + } + @if ((userCanArchive$ | async) && cipher.canBeArchived) { + + } + @if (canDeleteCipher$ | async) { + + } + diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 3d4fdb2e9f9..dd2c3e0252c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -1,9 +1,13 @@ -import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing"; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of, Subject } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AUTOFILL_ID, @@ -11,20 +15,32 @@ import { COPY_USERNAME_ID, COPY_VERIFICATION_CODE_ID, } from "@bitwarden/common/autofill/constants"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; 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"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherRiskService } from "@bitwarden/common/vault/abstractions/cipher-risk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; 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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CopyCipherFieldService, PasswordRepromptService } from "@bitwarden/vault"; +import { + ArchiveCipherUtilitiesService, + CopyCipherFieldService, + PasswordRepromptService, +} from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/browser/browser-popup-utils"; @@ -62,7 +78,10 @@ describe("ViewV2Component", () => { username: "test-username", password: "test-password", totp: "123", + uris: ["https://example.com"], }, + permissions: {}, + card: {}, } as unknown as CipherView; const mockPasswordRepromptService = { @@ -84,6 +103,8 @@ describe("ViewV2Component", () => { softDeleteWithServer: jest.fn().mockResolvedValue(undefined), }; + const cipherArchiveService = mock(); + beforeEach(async () => { mockCipherService.cipherViews$.mockClear(); mockCipherService.deleteWithServer.mockClear(); @@ -97,6 +118,10 @@ describe("ViewV2Component", () => { back.mockClear(); showToast.mockClear(); showPasswordPrompt.mockClear(); + cipherArchiveService.hasArchiveFlagEnabled$ = of(true); + cipherArchiveService.userCanArchive$.mockReturnValue(of(false)); + cipherArchiveService.archiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); + cipherArchiveService.unarchiveWithServer.mockResolvedValue({ id: "122-333-444" } as CipherData); await TestBed.configureTestingModule({ imports: [ViewV2Component], @@ -131,7 +156,7 @@ describe("ViewV2Component", () => { { provide: CipherAuthorizationService, useValue: { - canDeleteCipher$: jest.fn().mockReturnValue(true), + canDeleteCipher$: jest.fn().mockReturnValue(of(true)), }, }, { @@ -142,6 +167,61 @@ describe("ViewV2Component", () => { provide: PasswordRepromptService, useValue: mockPasswordRepromptService, }, + { + provide: CipherArchiveService, + useValue: cipherArchiveService, + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: CollectionService, + useValue: mock(), + }, + { + provide: FolderService, + useValue: mock(), + }, + { + provide: TaskService, + useValue: mock(), + }, + { + provide: ApiService, + useValue: mock(), + }, + { + provide: EnvironmentService, + useValue: { + environment$: of({ + getIconsUrl: () => "https://example.com", + }), + }, + }, + { + provide: DomainSettingsService, + useValue: { + showFavicons$: of(true), + }, + }, + { + provide: BillingAccountProfileStateService, + useValue: { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(false)), + }, + }, + { + provide: ArchiveCipherUtilitiesService, + useValue: { + archiveCipher: jest.fn().mockResolvedValue(null), + unarchiveCipher: jest.fn().mockResolvedValue(null), + }, + }, + { + provide: CipherRiskService, + useValue: mock(), + }, ], }) .overrideProvider(DialogService, { @@ -154,6 +234,7 @@ describe("ViewV2Component", () => { fixture = TestBed.createComponent(ViewV2Component); component = fixture.componentInstance; fixture.detectChanges(); + (component as any).showFooter$ = of(true); }); describe("queryParams", () => { @@ -352,6 +433,93 @@ describe("ViewV2Component", () => { })); }); + describe("archive button", () => { + it("shows the archive button when the user can archive and the cipher can be archived", fakeAsync(() => { + jest.spyOn(component["archiveService"], "userCanArchive$").mockReturnValueOnce(of(true)); + component.cipher = { ...mockCipher, canBeArchived: true } as CipherView; + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeTruthy(); + })); + + it("does not show the archive button when the user cannot archive", fakeAsync(() => { + jest.spyOn(component["archiveService"], "userCanArchive$").mockReturnValueOnce(of(false)); + component.cipher = { ...mockCipher, canBeArchived: true, isDeleted: false } as CipherView; + + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeFalsy(); + })); + + it("does not show the archive button when the cipher cannot be archived", fakeAsync(() => { + jest.spyOn(component["archiveService"], "userCanArchive$").mockReturnValueOnce(of(true)); + component.cipher = { ...mockCipher, archivedDate: new Date(), edit: true } as CipherView; + + tick(); + fixture.detectChanges(); + + const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']")); + expect(archiveBtn).toBeFalsy(); + })); + }); + + describe("unarchive button", () => { + it("shows the unarchive button when the cipher is archived", fakeAsync(() => { + component.cipher = { ...mockCipher, isArchived: true } as CipherView; + + tick(); + fixture.detectChanges(); + + const unarchiveBtn = fixture.debugElement.query( + By.css("button[biticonbutton='bwi-unarchive']"), + ); + expect(unarchiveBtn).toBeTruthy(); + })); + + it("does not show the unarchive button when the cipher is not archived", fakeAsync(() => { + component.cipher = { ...mockCipher, archivedDate: undefined } as CipherView; + + tick(); + fixture.detectChanges(); + + const unarchiveBtn = fixture.debugElement.query( + By.css("button[biticonbutton='bwi-unarchive']"), + ); + expect(unarchiveBtn).toBeFalsy(); + })); + }); + + describe("archive", () => { + beforeEach(() => { + component.cipher = { ...mockCipher, canBeArchived: true } as CipherView; + }); + + it("calls archive service to archive the cipher", async () => { + await component.archive(); + + expect(component["archiveCipherUtilsService"].archiveCipher).toHaveBeenCalledWith( + expect.objectContaining({ id: "122-333-444" }), + true, + ); + }); + }); + + describe("unarchive", () => { + it("calls archive service to unarchive the cipher", async () => { + component.cipher = { ...mockCipher, isArchived: true } as CipherView; + + await component.unarchive(); + + expect(component["archiveCipherUtilsService"].unarchiveCipher).toHaveBeenCalledWith( + expect.objectContaining({ id: "122-333-444" }), + ); + }); + }); + describe("delete", () => { beforeEach(() => { component.cipher = mockCipher; @@ -477,4 +645,44 @@ describe("ViewV2Component", () => { }); }); }); + + describe("archived badge", () => { + it("shows archived badge if the cipher is archived", fakeAsync(() => { + component.cipher = { ...mockCipher, isArchived: true } as CipherView; + mockCipherService.cipherViews$.mockImplementationOnce(() => + of([ + { + ...mockCipher, + isArchived: true, + }, + ]), + ); + + params$.next({ action: "view", cipherId: mockCipher.id }); + + flush(); + + fixture.detectChanges(); + + const badge = fixture.nativeElement.querySelector("span[bitBadge]"); + expect(badge).toBeTruthy(); + })); + + it("does not show archived badge if the cipher is not archived", () => { + component.cipher = { ...mockCipher, isArchived: false } as CipherView; + mockCipherService.cipherViews$.mockImplementationOnce(() => + of([ + { + ...mockCipher, + archivedDate: new Date(), + }, + ]), + ); + + fixture.detectChanges(); + + const badge = fixture.nativeElement.querySelector("span[bitBadge]"); + expect(badge).toBeFalsy(); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 1dea91c0b9f..f57b3e2d7f1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -7,9 +7,9 @@ import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, Observable, switchMap, of, map } from "rxjs"; -import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -25,6 +25,7 @@ import { EventType } from "@bitwarden/common/enums"; 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"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; @@ -34,6 +35,7 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { AsyncActionsModule, + BadgeModule, ButtonModule, CalloutModule, DialogService, @@ -42,6 +44,7 @@ import { ToastService, } from "@bitwarden/components"; import { + ArchiveCipherUtilitiesService, ChangeLoginPasswordService, CipherViewComponent, CopyCipherFieldService, @@ -58,6 +61,7 @@ import { BrowserPremiumUpgradePromptService } from "../../../services/browser-pr import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.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-v2.component"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; @@ -95,6 +99,7 @@ type LoadAction = AsyncActionsModule, PopOutComponent, CalloutModule, + BadgeModule, ], providers: [ { provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService }, @@ -112,8 +117,13 @@ export class ViewV2Component { collections$: Observable; loadAction: LoadAction; senderTabId?: number; + routeAfterDeletion?: ROUTES_AFTER_EDIT_DELETION; protected showFooter$: Observable; + protected userCanArchive$ = this.accountService.activeAccount$ + .pipe(getUserId) + .pipe(switchMap((userId) => this.archiveService.userCanArchive$(userId))); + protected archiveFlagEnabled$ = this.archiveService.hasArchiveFlagEnabled$; constructor( private passwordRepromptService: PasswordRepromptService, @@ -131,6 +141,8 @@ export class ViewV2Component { protected cipherAuthorizationService: CipherAuthorizationService, private copyCipherFieldService: CopyCipherFieldService, private popupScrollPositionService: VaultPopupScrollPositionService, + private archiveService: CipherArchiveService, + private archiveCipherUtilsService: ArchiveCipherUtilitiesService, ) { this.subscribeToParams(); } @@ -141,6 +153,9 @@ export class ViewV2Component { switchMap(async (params) => { this.loadAction = params.action; this.senderTabId = params.senderTabId ? parseInt(params.senderTabId, 10) : undefined; + this.routeAfterDeletion = params.routeAfterDeletion + ? params.routeAfterDeletion + : undefined; this.activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), @@ -220,7 +235,12 @@ export class ViewV2Component { return false; } void this.router.navigate(["/edit-cipher"], { - queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false }, + queryParams: { + cipherId: this.cipher.id, + type: this.cipher.type, + isNew: false, + routeAfterDeletion: this.routeAfterDeletion, + }, }); return true; } @@ -272,6 +292,24 @@ export class ViewV2Component { }); }; + archive = async () => { + const cipherResponse = await this.archiveCipherUtilsService.archiveCipher(this.cipher, true); + + if (!cipherResponse) { + return; + } + this.cipher.archivedDate = new Date(cipherResponse.archivedDate); + }; + + unarchive = async () => { + const cipherResponse = await this.archiveCipherUtilsService.unarchiveCipher(this.cipher); + + if (!cipherResponse) { + return; + } + this.cipher.archivedDate = null; + }; + protected deleteCipher() { return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, this.activeUserId) diff --git a/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts b/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts index 03111859165..1b279e1078d 100644 --- a/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts +++ b/apps/browser/src/vault/popup/guards/at-risk-passwords.guard.ts @@ -1,7 +1,13 @@ import { inject } from "@angular/core"; -import { CanActivateFn, Router } from "@angular/router"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; import { combineLatest, map, switchMap } from "rxjs"; +import { authGuard } from "@bitwarden/angular/auth/guards"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -9,6 +15,24 @@ import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; +/** + * Wrapper around the main auth guard to redirect to login if not authenticated. + * This is necessary because the main auth guard returns false when not authenticated, + * which in a browser context may result in a blank extension page rather than a redirect. + */ +export const atRiskPasswordAuthGuard: CanActivateFn = async ( + route: ActivatedRouteSnapshot, + routerState: RouterStateSnapshot, +) => { + const router = inject(Router); + + const authGuardResponse = await authGuard(route, routerState); + if (authGuardResponse === true) { + return authGuardResponse; + } + return router.createUrlTree(["/login"]); +}; + export const canAccessAtRiskPasswords: CanActivateFn = () => { const accountService = inject(AccountService); const taskService = inject(TaskService); 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 513e159f7aa..7cd73279c3d 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 @@ -3,8 +3,9 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 692e21d0084..1358c5faebe 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -3,12 +3,13 @@ import { TestBed, discardPeriodicTasks, fakeAsync, tick } from "@angular/core/te import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, skipWhile } from "rxjs"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import * as vaultFilterSvc from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; @@ -822,7 +823,6 @@ function createSeededVaultPopupListFiltersService( accountServiceMock, viewCacheServiceMock, restrictedItemTypesServiceMock, - configService, ); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 08db7d5d4ab..85c415d01fe 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -14,23 +14,21 @@ import { take, } from "rxjs"; -import { - CollectionService, - CollectionTypes, - CollectionView, -} from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/view-cache"; import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.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, + CollectionTypes, +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -191,7 +189,6 @@ export class VaultPopupListFiltersService { private accountService: AccountService, private viewCacheService: ViewCacheService, private restrictedItemTypesService: RestrictedItemTypesService, - private configService: ConfigService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) @@ -455,19 +452,15 @@ export class VaultPopupListFiltersService { ), this.collectionService.decryptedCollections$(userId), this.organizationService.memberOrganizations$(userId), - this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation), ]), ), - map(([filters, allCollections, orgs, defaultVaultEnabled]) => { + map(([filters, allCollections, orgs]) => { const orgFilterId = filters.organization?.id ?? null; // When the organization filter is selected, filter out collections that do not belong to the selected organization const filtered = orgFilterId ? allCollections.filter((c) => c.organizationId === orgFilterId) : allCollections; - if (!defaultVaultEnabled) { - return filtered; - } return sortDefaultCollections(filtered, orgs, this.i18nService.collator); }), map((fullList) => { diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.html b/apps/browser/src/vault/popup/settings/admin-settings.component.html new file mode 100644 index 00000000000..5e67750278f --- /dev/null +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.html @@ -0,0 +1,41 @@ + + + + + + + +
+ @if (showAutoConfirmSpotlight$ | async) { + +
+ + {{ "autoConfirmOnboardingCallout" | i18n }} + + + +
+
+ } + +
+ + + + + {{ "automaticUserConfirmation" | i18n }} + + + {{ "automaticUserConfirmationHint" | i18n }} + + +
+
+
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 new file mode 100644 index 00000000000..f7b4e7b473a --- /dev/null +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.spec.ts @@ -0,0 +1,199 @@ +import { ChangeDetectionStrategy, Component, input } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { mock, MockProxy } from "jest-mock-extended"; +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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; + +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +import { AdminSettingsComponent } from "./admin-settings.component"; + +@Component({ + selector: "popup-header", + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class MockPopupHeaderComponent { + readonly pageTitle = input(); + readonly backAction = input<() => void>(); +} + +@Component({ + selector: "popup-page", + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class MockPopupPageComponent { + readonly loading = input(); +} + +@Component({ + selector: "app-pop-out", + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class MockPopOutComponent { + readonly show = input(true); +} + +describe("AdminSettingsComponent", () => { + let component: AdminSettingsComponent; + let fixture: ComponentFixture; + let autoConfirmService: MockProxy; + let nudgesService: MockProxy; + let mockDialogService: MockProxy; + + const userId = "test-user-id" as UserId; + const mockAutoConfirmState: AutoConfirmState = { + enabled: false, + showSetupDialog: true, + showBrowserNotification: false, + }; + + beforeEach(async () => { + autoConfirmService = mock(); + nudgesService = mock(); + mockDialogService = mock(); + + autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState)); + autoConfirmService.upsert.mockResolvedValue(undefined); + nudgesService.showNudgeSpotlight$.mockReturnValue(of(false)); + + await TestBed.configureTestingModule({ + imports: [AdminSettingsComponent], + providers: [ + provideNoopAnimations(), + { provide: AccountService, useValue: mockAccountServiceWith(userId) }, + { provide: AutomaticUserConfirmationService, useValue: autoConfirmService }, + { provide: DialogService, useValue: mockDialogService }, + { provide: NudgesService, useValue: nudgesService }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }) + .overrideComponent(AdminSettingsComponent, { + remove: { + imports: [PopupHeaderComponent, PopupPageComponent, PopOutComponent], + }, + add: { + imports: [MockPopupHeaderComponent, MockPopupPageComponent, MockPopOutComponent], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminSettingsComponent); + component = fixture.componentInstance; + }); + + describe("initialization", () => { + it("should populate form with current auto-confirm state", async () => { + const mockState: AutoConfirmState = { + enabled: true, + showSetupDialog: false, + showBrowserNotification: true, + }; + autoConfirmService.configuration$.mockReturnValue(of(mockState)); + + await component.ngOnInit(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component["adminForm"].value).toEqual({ + autoConfirm: true, + }); + }); + + it("should populate form with disabled auto-confirm state", async () => { + await component.ngOnInit(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component["adminForm"].value).toEqual({ + autoConfirm: false, + }); + }); + }); + + describe("spotlight", () => { + beforeEach(async () => { + await component.ngOnInit(); + fixture.detectChanges(); + }); + + it("should expose showAutoConfirmSpotlight$ observable", (done) => { + nudgesService.showNudgeSpotlight$.mockReturnValue(of(true)); + + const newFixture = TestBed.createComponent(AdminSettingsComponent); + const newComponent = newFixture.componentInstance; + + newComponent["showAutoConfirmSpotlight$"].subscribe((show) => { + expect(show).toBe(true); + expect(nudgesService.showNudgeSpotlight$).toHaveBeenCalledWith( + NudgeType.AutoConfirmNudge, + userId, + ); + done(); + }); + }); + + it("should dismiss spotlight and update state", async () => { + autoConfirmService.upsert.mockResolvedValue(); + + await component.dismissSpotlight(); + + expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, { + ...mockAutoConfirmState, + showBrowserNotification: false, + }); + }); + + it("should use current userId when dismissing spotlight", async () => { + autoConfirmService.upsert.mockResolvedValue(); + + await component.dismissSpotlight(); + + expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, expect.any(Object)); + }); + + it("should preserve existing state when dismissing spotlight", async () => { + const customState: AutoConfirmState = { + enabled: true, + showSetupDialog: false, + showBrowserNotification: true, + }; + autoConfirmService.configuration$.mockReturnValue(of(customState)); + autoConfirmService.upsert.mockResolvedValue(); + + await component.dismissSpotlight(); + + expect(autoConfirmService.upsert).toHaveBeenCalledWith(userId, { + ...customState, + showBrowserNotification: false, + }); + }); + }); + + describe("form validation", () => { + beforeEach(async () => { + await component.ngOnInit(); + fixture.detectChanges(); + }); + + it("should have a valid form", () => { + expect(component["adminForm"].valid).toBe(true); + }); + + it("should have autoConfirm control", () => { + expect(component["adminForm"].controls.autoConfirm).toBeDefined(); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts new file mode 100644 index 00000000000..e4b676525ed --- /dev/null +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -0,0 +1,121 @@ +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + OnInit, + signal, + WritableSignal, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom, map, Observable, of, switchMap, tap, withLatestFrom } from "rxjs"; + +import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; +import { + AutoConfirmWarningDialogComponent, + AutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; +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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { + BitIconButtonComponent, + CardComponent, + DialogService, + FormFieldModule, + SwitchComponent, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { UserId } from "@bitwarden/user-core"; + +@Component({ + templateUrl: "./admin-settings.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + FormFieldModule, + ReactiveFormsModule, + SwitchComponent, + CardComponent, + SpotlightComponent, + BitIconButtonComponent, + I18nPipe, + ], +}) +export class AdminSettingsComponent implements OnInit { + private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); + + protected readonly formLoading: WritableSignal = signal(true); + protected adminForm = this.formBuilder.group({ + autoConfirm: false, + }); + protected showAutoConfirmSpotlight$: Observable = this.userId$.pipe( + switchMap((userId) => + this.nudgesService.showNudgeSpotlight$(NudgeType.AutoConfirmNudge, userId), + ), + ); + + constructor( + private formBuilder: FormBuilder, + private accountService: AccountService, + private autoConfirmService: AutomaticUserConfirmationService, + private destroyRef: DestroyRef, + private dialogService: DialogService, + private nudgesService: NudgesService, + ) {} + + async ngOnInit() { + const userId = await firstValueFrom(this.userId$); + const autoConfirmEnabled = ( + await firstValueFrom(this.autoConfirmService.configuration$(userId)) + ).enabled; + this.adminForm.setValue({ autoConfirm: autoConfirmEnabled }); + + this.formLoading.set(false); + + this.adminForm.controls.autoConfirm.valueChanges + .pipe( + switchMap((newValue) => { + if (newValue) { + return this.confirm(); + } + return of(false); + }), + withLatestFrom(this.autoConfirmService.configuration$(userId)), + switchMap(([newValue, existingState]) => + this.autoConfirmService.upsert(userId, { + ...existingState, + enabled: newValue, + showBrowserNotification: false, + }), + ), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + } + + private confirm(): Observable { + return AutoConfirmWarningDialogComponent.open(this.dialogService).closed.pipe( + map((result) => result ?? false), + tap((result) => { + if (!result) { + this.adminForm.setValue({ autoConfirm: false }, { emitEvent: false }); + } + }), + ); + } + + async dismissSpotlight() { + const userId = await firstValueFrom(this.userId$); + const state = await firstValueFrom(this.autoConfirmService.configuration$(userId)); + + await this.autoConfirmService.upsert(userId, { ...state, showBrowserNotification: false }); + } +} diff --git a/apps/browser/src/vault/popup/settings/archive.component.html b/apps/browser/src/vault/popup/settings/archive.component.html index a7b23dc5122..16afab4384b 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.html +++ b/apps/browser/src/vault/popup/settings/archive.component.html @@ -5,6 +5,21 @@ + @if (showSubscriptionEndedMessaging$ | async) { + +
+ +

{{ "premiumSubscriptionEnded" | i18n }}

+
+

+ {{ "archivePremiumRestart" | i18n }} +

+ +
+ } + @if (archivedCiphers$ | async; as archivedItems) { @if (archivedItems.length) { @@ -48,6 +63,15 @@ + @if (canAssignCollections$ | async) { + + } diff --git a/apps/browser/src/vault/popup/settings/archive.component.spec.ts b/apps/browser/src/vault/popup/settings/archive.component.spec.ts new file mode 100644 index 00000000000..2f5cfb8d824 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/archive.component.spec.ts @@ -0,0 +1,140 @@ +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { PopupRouterCacheService } from "@bitwarden/browser/platform/popup/view-cache/popup-router-cache.service"; +import { OrganizationService } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { ArchiveComponent } from "./archive.component"; + +// 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile. +// Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the +// `BrowserTotpCaptureService` where jest would not load the file in the first place. +jest.mock("qrcode-parser", () => {}); + +describe("ArchiveComponent", () => { + let component: ArchiveComponent; + + let hasOrganizations: jest.Mock; + let decryptedCollections$: jest.Mock; + let navigate: jest.Mock; + let showPasswordPrompt: jest.Mock; + + beforeAll(async () => { + navigate = jest.fn(); + showPasswordPrompt = jest.fn().mockResolvedValue(true); + hasOrganizations = jest.fn(); + decryptedCollections$ = jest.fn(); + + await TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: { navigate } }, + { + provide: AccountService, + useValue: { activeAccount$: new BehaviorSubject({ id: "user-id" }) }, + }, + { provide: PasswordRepromptService, useValue: { showPasswordPrompt } }, + { provide: OrganizationService, useValue: { hasOrganizations } }, + { provide: CollectionService, useValue: { decryptedCollections$ } }, + { provide: DialogService, useValue: mock() }, + { provide: CipherService, useValue: mock() }, + { provide: CipherArchiveService, useValue: mock() }, + { provide: ToastService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: LogService, useValue: mock() }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(ArchiveComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("canAssignCollections$", () => { + it("emits true when user has organizations and editable collections", (done) => { + hasOrganizations.mockReturnValue(of(true)); + decryptedCollections$.mockReturnValue(of([{ id: "col-1", readOnly: false }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("emits false when user has no organizations", (done) => { + hasOrganizations.mockReturnValue(of(false)); + decryptedCollections$.mockReturnValue(of([{ id: "col-1", readOnly: false }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + + it("emits false when all collections are read-only", (done) => { + hasOrganizations.mockReturnValue(of(true)); + decryptedCollections$.mockReturnValue(of([{ id: "col-1", readOnly: true }] as any)); + + component["canAssignCollections$"].subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + }); + + describe("conditionallyNavigateToAssignCollections", () => { + const mockCipher = { + id: "cipher-1", + reprompt: 0, + } as CipherViewLike; + + it("navigates to assign-collections when reprompt is not required", async () => { + await component.conditionallyNavigateToAssignCollections(mockCipher); + + expect(navigate).toHaveBeenCalledWith(["/assign-collections"], { + queryParams: { cipherId: "cipher-1" }, + }); + }); + + it("prompts for password when reprompt is required", async () => { + const cipherWithReprompt = { ...mockCipher, reprompt: 1 }; + + await component.conditionallyNavigateToAssignCollections( + cipherWithReprompt as CipherViewLike, + ); + + expect(showPasswordPrompt).toHaveBeenCalled(); + expect(navigate).toHaveBeenCalledWith(["/assign-collections"], { + queryParams: { cipherId: "cipher-1" }, + }); + }); + + it("does not navigate when password prompt is cancelled", async () => { + const cipherWithReprompt = { ...mockCipher, reprompt: 1 }; + showPasswordPrompt.mockResolvedValueOnce(false); + + await component.conditionallyNavigateToAssignCollections( + cipherWithReprompt as CipherViewLike, + ); + + expect(showPasswordPrompt).toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index b1c78444a3f..ecf091a7322 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -1,9 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, map, Observable, startWith, switchMap } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, startWith, switchMap } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +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"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -25,6 +27,8 @@ import { SectionHeaderComponent, ToastService, TypographyModule, + CardComponent, + ButtonComponent, } from "@bitwarden/components"; import { CanDeleteCipherDirective, @@ -35,6 +39,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-v2/add-edit/add-edit-v2.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -55,6 +60,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co SectionComponent, SectionHeaderComponent, TypographyModule, + CardComponent, + ButtonComponent, ], }) export class ArchiveComponent { @@ -67,6 +74,8 @@ export class ArchiveComponent { private i18nService = inject(I18nService); private cipherArchiveService = inject(CipherArchiveService); private passwordRepromptService = inject(PasswordRepromptService); + private organizationService = inject(OrganizationService); + private collectionService = inject(CollectionService); private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); @@ -74,6 +83,9 @@ export class ArchiveComponent { switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)), ); + protected userCanArchive$ = this.userId$.pipe( + switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)), + ); protected CipherViewLikeUtils = CipherViewLikeUtils; protected loading$ = this.archivedCiphers$.pipe( @@ -81,13 +93,39 @@ export class ArchiveComponent { startWith(true), ); + protected canAssignCollections$ = this.userId$.pipe( + switchMap((userId) => { + return combineLatest([ + this.organizationService.hasOrganizations(userId), + this.collectionService.decryptedCollections$(userId), + ]).pipe( + map(([hasOrgs, collections]) => { + const canEditCollections = collections.some((c) => !c.readOnly); + return hasOrgs && canEditCollections; + }), + ); + }), + ); + + protected showSubscriptionEndedMessaging$ = this.userId$.pipe( + switchMap((userId) => this.cipherArchiveService.showSubscriptionEndedMessaging$(userId)), + ); + + async navigateToPremium() { + await this.router.navigate(["/premium"]); + } + async view(cipher: CipherViewLike) { if (!(await this.canInteract(cipher))) { return; } await this.router.navigate(["/view-cipher"], { - queryParams: { cipherId: cipher.id, type: cipher.type }, + queryParams: { + cipherId: cipher.id, + type: cipher.type, + routeAfterDeletion: ROUTES_AFTER_EDIT_DELETION.archive, + }, }); } @@ -97,7 +135,11 @@ export class ArchiveComponent { } await this.router.navigate(["/edit-cipher"], { - queryParams: { cipherId: cipher.id, type: cipher.type }, + queryParams: { + cipherId: cipher.id, + type: cipher.type, + routeAfterDeletion: ROUTES_AFTER_EDIT_DELETION.archive, + }, }); } @@ -173,6 +215,17 @@ export class ArchiveComponent { }); } + /** Prompts for password when necessary then navigates to the assign collections route */ + async conditionallyNavigateToAssignCollections(cipher: CipherViewLike) { + if (cipher.reprompt && !(await this.passwordRepromptService.showPasswordPrompt())) { + return; + } + + await this.router.navigate(["/assign-collections"], { + queryParams: { cipherId: cipher.id }, + }); + } + /** * Check if the user is able to interact with the cipher * (password re-prompt / decryption failure checks). diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index d5b94df5008..ad009c7a60b 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -52,9 +52,7 @@ > {{ "archiveNoun" | i18n }} - @if (!userHasArchivedItems()) { - - } + diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts index 15ddb7507fd..554570de7f9 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.spec.ts @@ -195,10 +195,10 @@ describe("VaultSettingsV2Component", () => { expect(component["userHasArchivedItems"]()).toBe(false); }); - it("hides premium badge when user has archived items", () => { + it("shows premium badge when user has archived items but cannot archive", () => { setArchiveState(false, [{ id: "cipher1" } as CipherView]); - expect(component["premiumBadgeComponent"]()).toBeUndefined(); + expect(component["premiumBadgeComponent"]()).toBeTruthy(); expect(component["userHasArchivedItems"]()).toBe(true); }); }); diff --git a/apps/browser/src/vault/popup/views/popup-cipher.view.ts b/apps/browser/src/vault/popup/views/popup-cipher.view.ts index 6f85e7b6eb4..7d035ceb6df 100644 --- a/apps/browser/src/vault/popup/views/popup-cipher.view.ts +++ b/apps/browser/src/vault/popup/views/popup-cipher.view.ts @@ -1,4 +1,4 @@ -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherListView } from "@bitwarden/sdk-internal"; diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index adae5091172..4458054e244 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -9,9 +9,7 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -28,7 +26,6 @@ export class ConfirmCommand { private encryptService: EncryptService, private organizationUserApiService: OrganizationUserApiService, private accountService: AccountService, - private configService: ConfigService, private i18nService: I18nService, ) {} @@ -80,11 +77,7 @@ export class ConfirmCommand { const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const req = new OrganizationUserConfirmRequest(); req.key = key.encryptedString; - if ( - await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) - ) { - req.defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); - } + req.defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey); await this.organizationUserApiService.postOrganizationUserConfirm( options.organizationId, id, diff --git a/apps/cli/src/admin-console/models/response/collection.response.ts b/apps/cli/src/admin-console/models/response/collection.response.ts index a0d1ce1047d..4c56fdcd84a 100644 --- a/apps/cli/src/admin-console/models/response/collection.response.ts +++ b/apps/cli/src/admin-console/models/response/collection.response.ts @@ -1,4 +1,4 @@ -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { CollectionWithIdExport } from "@bitwarden/common/models/export/collection-with-id.export"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/apps/cli/src/admin-console/models/response/organization-collection.response.ts b/apps/cli/src/admin-console/models/response/organization-collection.response.ts index a0d62b4c7b6..4b5c9a08f2b 100644 --- a/apps/cli/src/admin-console/models/response/organization-collection.response.ts +++ b/apps/cli/src/admin-console/models/response/organization-collection.response.ts @@ -1,4 +1,4 @@ -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { SelectionReadOnly } from "../selection-read-only"; diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts index 71c3830b4cc..2ce0d425007 100644 --- a/apps/cli/src/base-program.ts +++ b/apps/cli/src/base-program.ts @@ -172,9 +172,7 @@ export abstract class BaseProgram { } else { const command = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -184,7 +182,6 @@ export abstract class BaseProgram { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 35816b56fb2..db070344628 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -1,12 +1,11 @@ -// 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, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index ff210cf222d..2430035e34a 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -1,16 +1,15 @@ import { firstValueFrom, map } from "rxjs"; +import { OrganizationUserApiService, CollectionService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { - OrganizationUserApiService, - CollectionService, CollectionData, Collection, CollectionDetailsResponse as ApiCollectionDetailsResponse, CollectionResponse as ApiCollectionResponse, -} from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +} from "@bitwarden/common/admin-console/models/collections"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; diff --git a/apps/cli/src/key-management/commands/unlock.command.spec.ts b/apps/cli/src/key-management/commands/unlock.command.spec.ts index 50ef414ec37..a722469f7bb 100644 --- a/apps/cli/src/key-management/commands/unlock.command.spec.ts +++ b/apps/cli/src/key-management/commands/unlock.command.spec.ts @@ -3,21 +3,16 @@ import { of } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { MasterPasswordVerificationResponse } from "@bitwarden/common/auth/types/verification"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { ConsoleLogService } from "@bitwarden/logging"; import { UserId } from "@bitwarden/user-core"; @@ -32,9 +27,7 @@ describe("UnlockCommand", () => { let command: UnlockCommand; const accountService = mock(); - const masterPasswordService = mock(); const keyService = mock(); - const userVerificationService = mock(); const cryptoFunctionService = mock(); const logService = mock(); const keyConnectorService = mock(); @@ -44,7 +37,6 @@ describe("UnlockCommand", () => { const i18nService = mock(); const encryptedMigrator = mock(); const masterPasswordUnlockService = mock(); - const configService = mock(); const mockMasterPassword = "testExample"; const activeAccount: Account = { @@ -73,9 +65,6 @@ describe("UnlockCommand", () => { ); expectedSuccessMessage.raw = b64sessionKey; - // Legacy test data - const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey; - beforeEach(async () => { jest.clearAllMocks(); @@ -86,9 +75,7 @@ describe("UnlockCommand", () => { command = new UnlockCommand( accountService, - masterPasswordService, keyService, - userVerificationService, cryptoFunctionService, logService, keyConnectorService, @@ -98,7 +85,6 @@ describe("UnlockCommand", () => { i18nService, encryptedMigrator, masterPasswordUnlockService, - configService, ); }); @@ -133,116 +119,46 @@ describe("UnlockCommand", () => { }, ); - describe("UnlockWithMasterPasswordUnlockData feature flag enabled", () => { - beforeEach(() => { - configService.getFeatureFlag$.mockReturnValue(of(true)); - }); + it("calls masterPasswordUnlockService successfully", async () => { + masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); - it("calls masterPasswordUnlockService successfully", async () => { - masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - }); - - it("returns error response if unlockWithMasterPassword fails", async () => { - masterPasswordUnlockService.unlockWithMasterPassword.mockRejectedValue( - new Error("Unlock failed"), - ); - - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(false); - expect(response.message).toEqual("Unlock failed"); - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - expect(keyService.setUserKey).not.toHaveBeenCalled(); - }); + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(response.data).toEqual(expectedSuccessMessage); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); }); - describe("unlock with feature flag off", () => { - beforeEach(() => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - }); + it("returns error response if unlockWithMasterPassword fails", async () => { + masterPasswordUnlockService.unlockWithMasterPassword.mockRejectedValue( + new Error("Unlock failed"), + ); - it("calls decryptUserKeyWithMasterKey successfully", async () => { - userVerificationService.verifyUserByMasterPassword.mockResolvedValue({ - masterKey: mockMasterKey, - } as MasterPasswordVerificationResponse); - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - }); - - it("returns error response when verifyUserByMasterPassword throws", async () => { - userVerificationService.verifyUserByMasterPassword.mockRejectedValue( - new Error("Verification failed"), - ); - - const response = await command.run(mockMasterPassword, {}); - - expect(response).not.toBeNull(); - expect(response.success).toEqual(false); - expect(response.message).toEqual("Verification failed"); - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).not.toHaveBeenCalled(); - expect(keyService.setUserKey).not.toHaveBeenCalled(); - }); + expect(response).not.toBeNull(); + expect(response.success).toEqual(false); + expect(response.message).toEqual("Unlock failed"); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); describe("calls convertToKeyConnectorCommand if required", () => { let convertToKeyConnectorSpy: jest.SpyInstance; beforeEach(() => { keyConnectorService.convertAccountRequired$ = of(true); - - // Feature flag on masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey); - - // Feature flag off - userVerificationService.verifyUserByMasterPassword.mockResolvedValue({ - masterKey: mockMasterKey, - } as MasterPasswordVerificationResponse); - masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey); }); - test.each([true, false])("returns failure when feature flag is %s", async (flagValue) => { - configService.getFeatureFlag$.mockReturnValue(of(flagValue)); - + it("returns error on failure", async () => { // Mock the ConvertToKeyConnectorCommand const mockRun = jest.fn().mockResolvedValue({ success: false, message: "convert failed" }); convertToKeyConnectorSpy = jest @@ -257,67 +173,32 @@ describe("UnlockCommand", () => { expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - if (flagValue === true) { - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - } else { - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - } + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); }); - test.each([true, false])( - "returns expected success when feature flag is %s", - async (flagValue) => { - configService.getFeatureFlag$.mockReturnValue(of(flagValue)); + it("returns success on successful conversion", async () => { + // Mock the ConvertToKeyConnectorCommand + const mockRun = jest.fn().mockResolvedValue({ success: true }); + const convertToKeyConnectorSpy = jest + .spyOn(ConvertToKeyConnectorCommand.prototype, "run") + .mockImplementation(mockRun); - // Mock the ConvertToKeyConnectorCommand - const mockRun = jest.fn().mockResolvedValue({ success: true }); - const convertToKeyConnectorSpy = jest - .spyOn(ConvertToKeyConnectorCommand.prototype, "run") - .mockImplementation(mockRun); + const response = await command.run(mockMasterPassword, {}); - const response = await command.run(mockMasterPassword, {}); + expect(response).not.toBeNull(); + expect(response.success).toEqual(true); + expect(response.data).toEqual(expectedSuccessMessage); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); + expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - expect(response).not.toBeNull(); - expect(response.success).toEqual(true); - expect(response.data).toEqual(expectedSuccessMessage); - expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id); - expect(convertToKeyConnectorSpy).toHaveBeenCalled(); - - if (flagValue === true) { - expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( - mockMasterPassword, - activeAccount.id, - ); - } else { - expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith( - { - type: VerificationType.MasterPassword, - secret: mockMasterPassword, - }, - activeAccount.id, - activeAccount.email, - ); - expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockMasterKey, - activeAccount.id, - ); - } - }, - ); + expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith( + mockMasterPassword, + activeAccount.id, + ); + }); }); }); }); diff --git a/apps/cli/src/key-management/commands/unlock.command.ts b/apps/cli/src/key-management/commands/unlock.command.ts index c88d9ae1cc4..5f82b721d07 100644 --- a/apps/cli/src/key-management/commands/unlock.command.ts +++ b/apps/cli/src/key-management/commands/unlock.command.ts @@ -4,20 +4,13 @@ import { firstValueFrom } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; -import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { MasterKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; import { Response } from "../../models/response"; @@ -29,9 +22,7 @@ import { ConvertToKeyConnectorCommand } from "../convert-to-key-connector.comman export class UnlockCommand { constructor( private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private keyService: KeyService, - private userVerificationService: UserVerificationService, private cryptoFunctionService: CryptoFunctionService, private logService: ConsoleLogService, private keyConnectorService: KeyConnectorService, @@ -41,7 +32,6 @@ export class UnlockCommand { private i18nService: I18nService, private encryptedMigrator: EncryptedMigrator, private masterPasswordUnlockService: MasterPasswordUnlockService, - private configService: ConfigService, ) {} async run(password: string, cmdOptions: Record) { @@ -61,46 +51,15 @@ export class UnlockCommand { } const userId = activeAccount.id; - if ( - await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.UnlockWithMasterPasswordUnlockData), - ) - ) { - try { - const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword( - password, - userId, - ); - - await this.keyService.setUserKey(userKey, userId); - } catch (e) { - return Response.error(e.message); - } - } else { - const email = activeAccount.email; - const verification = { - type: VerificationType.MasterPassword, - secret: password, - } as MasterPasswordVerification; - - let masterKey: MasterKey; - try { - const response = await this.userVerificationService.verifyUserByMasterPassword( - verification, - userId, - email, - ); - masterKey = response.masterKey; - } catch (e) { - // verification failure throws - return Response.error(e.message); - } - - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - masterKey, + try { + const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword( + password, userId, ); + await this.keyService.setUserKey(userKey, userId); + } catch (e) { + return Response.error(e.message); } if (await firstValueFrom(this.keyConnectorService.convertAccountRequired$)) { diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index e8f5e6acd9a..e0385534cb7 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -147,7 +147,6 @@ export class OssServeConfigurator { this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, this.serviceContainer.accountService, - this.serviceContainer.configService, this.serviceContainer.i18nService, ); this.restoreCommand = new RestoreCommand( @@ -167,9 +166,7 @@ export class OssServeConfigurator { ); this.unlockCommand = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -179,7 +176,6 @@ export class OssServeConfigurator { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 870d743095d..7856fc3588c 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -303,9 +303,7 @@ export class Program extends BaseProgram { await this.exitIfNotAuthed(); const command = new UnlockCommand( this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, this.serviceContainer.keyService, - this.serviceContainer.userVerificationService, this.serviceContainer.cryptoFunctionService, this.serviceContainer.logService, this.serviceContainer.keyConnectorService, @@ -315,7 +313,6 @@ export class Program extends BaseProgram { this.serviceContainer.i18nService, this.serviceContainer.encryptedMigrator, this.serviceContainer.masterPasswordUnlockService, - this.serviceContainer.configService, ); const response = await command.run(password, cmd); this.processResponse(response); diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index 7803f6f94d4..91e579c26c1 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -9,9 +9,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { NodeUtils } from "@bitwarden/node/node-utils"; import { Response } from "../../../models/response"; diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts index bf53c8a5cb9..2c6d41d66ac 100644 --- a/apps/cli/src/tools/send/commands/edit.command.ts +++ b/apps/cli/src/tools/send/commands/edit.command.ts @@ -5,9 +5,9 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { Response } from "../../../models/response"; import { CliUtils } from "../../../utils"; diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index a412f7c1667..5cbf458c87f 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -13,11 +13,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { KeyService } from "@bitwarden/key-management"; import { NodeUtils } from "@bitwarden/node/node-utils"; diff --git a/apps/cli/src/tools/send/commands/template.command.ts b/apps/cli/src/tools/send/commands/template.command.ts index c1c2c97b03d..09213ac5fa8 100644 --- a/apps/cli/src/tools/send/commands/template.command.ts +++ b/apps/cli/src/tools/send/commands/template.command.ts @@ -1,4 +1,4 @@ -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { Response } from "../../../models/response"; import { TemplateResponse } from "../../../models/response/template.response"; diff --git a/apps/cli/src/tools/send/models/send-access.response.ts b/apps/cli/src/tools/send/models/send-access.response.ts index 07877bfb548..7bd54801307 100644 --- a/apps/cli/src/tools/send/models/send-access.response.ts +++ b/apps/cli/src/tools/send/models/send-access.response.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/apps/cli/src/tools/send/models/send.response.ts b/apps/cli/src/tools/send/models/send.response.ts index a0c1d3f83c6..b7655226be0 100644 --- a/apps/cli/src/tools/send/models/send.response.ts +++ b/apps/cli/src/tools/send/models/send.response.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { BaseResponse } from "../../../models/response/base.response"; diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 33bf4518ccd..869d77a379c 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -7,7 +7,7 @@ import * as chalk from "chalk"; import { program, Command, Option, OptionValues } from "commander"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { BaseProgram } from "../../base-program"; import { Response } from "../../models/response"; diff --git a/apps/cli/src/utils.ts b/apps/cli/src/utils.ts index e321adbfd5e..72746cb9b71 100644 --- a/apps/cli/src/utils.ts +++ b/apps/cli/src/utils.ts @@ -1,12 +1,10 @@ -// 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"; import * as inquirer from "inquirer"; import * as JSZip from "jszip"; -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 21f87feab00..3e08038fe64 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -494,7 +494,6 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, this.serviceContainer.accountService, - this.serviceContainer.configService, this.serviceContainer.i18nService, ); const response = await command.run(object, id, cmd); diff --git a/apps/desktop/custom-appx-manifest.xml b/apps/desktop/custom-appx-manifest.xml new file mode 100644 index 00000000000..2f7796c97cf --- /dev/null +++ b/apps/desktop/custom-appx-manifest.xml @@ -0,0 +1,111 @@ + + + + + + + + ${displayName} + ${publisherDisplayName} + A secure and free password manager for all of your devices. + assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index f5e5cf7ee18..879c5621105 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -386,9 +386,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitwarden-russh" @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -1180,9 +1180,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -1671,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.3", ] [[package]] @@ -1731,19 +1731,18 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.25" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "macos_provider" @@ -1994,11 +1993,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2258,9 +2256,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2268,15 +2266,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2653,9 +2651,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -3011,9 +3009,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smawk" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 86eb507a6c1..fec4dc41982 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -9,7 +9,7 @@ members = [ "napi", "process_isolation", "proxy", - "windows_plugin_authenticator" + "windows_plugin_authenticator", ] [workspace.package] @@ -50,7 +50,7 @@ oo7 = "=0.5.0" pin-project = "=1.1.10" pkcs8 = "=0.10.2" rand = "=0.9.2" -rsa = "=0.9.6" +rsa = "=0.9.10" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" secmem-proc = "=0.3.7" diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml index 6bf3218d98a..b0f78ca2f20 100644 --- a/apps/desktop/desktop_native/autotype/Cargo.toml +++ b/apps/desktop/desktop_native/autotype/Cargo.toml @@ -5,9 +5,6 @@ license.workspace = true edition.workspace = true publish.workspace = true -[dependencies] -anyhow = { workspace = true } - [target.'cfg(windows)'.dependencies] itertools.workspace = true mockall = "=0.14.0" @@ -19,5 +16,8 @@ windows = { workspace = true, features = [ ] } windows-core = { workspace = true } +[dependencies] +anyhow = { workspace = true } + [lints] workspace = true diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index 4b9e65180e6..917f0f797b6 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -1,5 +1,11 @@ use anyhow::Result; +#[cfg(target_os = "windows")] +mod modifier_keys; + +#[cfg(target_os = "windows")] +pub(crate) use modifier_keys::*; + #[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "windows", path = "windows/mod.rs")] diff --git a/apps/desktop/desktop_native/autotype/src/modifier_keys.rs b/apps/desktop/desktop_native/autotype/src/modifier_keys.rs new file mode 100644 index 00000000000..c451a3b25e4 --- /dev/null +++ b/apps/desktop/desktop_native/autotype/src/modifier_keys.rs @@ -0,0 +1,45 @@ +// Electron modifier keys +// +pub(crate) const CONTROL_KEY_STR: &str = "Control"; +pub(crate) const ALT_KEY_STR: &str = "Alt"; +pub(crate) const SUPER_KEY_STR: &str = "Super"; + +// numeric values for modifier keys +pub(crate) const CONTROL_KEY: u16 = 0x11; +pub(crate) const ALT_KEY: u16 = 0x12; +pub(crate) const SUPER_KEY: u16 = 0x5B; + +/// A mapping of to +static MODIFIER_KEYS: [(&str, u16); 3] = [ + (CONTROL_KEY_STR, CONTROL_KEY), + (ALT_KEY_STR, ALT_KEY), + (SUPER_KEY_STR, SUPER_KEY), +]; + +/// Provides a mapping of the valid modifier keys' electron +/// string representation to the numeric representation. +pub(crate) fn get_numeric_modifier_key(modifier: &str) -> Option { + for (modifier_str, modifier_num) in MODIFIER_KEYS { + if modifier_str == modifier { + return Some(modifier_num); + } + } + None +} + +#[cfg(test)] +mod test { + use super::get_numeric_modifier_key; + + #[test] + fn valid_modifier_keys() { + assert_eq!(get_numeric_modifier_key("Control").unwrap(), 0x11); + assert_eq!(get_numeric_modifier_key("Alt").unwrap(), 0x12); + assert_eq!(get_numeric_modifier_key("Super").unwrap(), 0x5B); + } + + #[test] + fn does_not_contain_invalid_modifier_keys() { + assert!(get_numeric_modifier_key("Shift").is_none()); + } +} diff --git a/apps/desktop/desktop_native/autotype/src/windows/mod.rs b/apps/desktop/desktop_native/autotype/src/windows/mod.rs index 9cd9bc0cbe5..ed985749303 100644 --- a/apps/desktop/desktop_native/autotype/src/windows/mod.rs +++ b/apps/desktop/desktop_native/autotype/src/windows/mod.rs @@ -44,7 +44,6 @@ pub fn get_foreground_window_title() -> Result { /// - Control /// - Alt /// - Super -/// - Shift /// - \[a-z\]\[A-Z\] struct KeyboardShortcutInput(INPUT); diff --git a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs index b62dd7290d1..f7879e676bf 100644 --- a/apps/desktop/desktop_native/autotype/src/windows/type_input.rs +++ b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs @@ -6,11 +6,7 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{ }; use super::{ErrorOperations, KeyboardShortcutInput, Win32ErrorOperations}; - -const SHIFT_KEY_STR: &str = "Shift"; -const CONTROL_KEY_STR: &str = "Control"; -const ALT_KEY_STR: &str = "Alt"; -const LEFT_WINDOWS_KEY_STR: &str = "Super"; +use crate::get_numeric_modifier_key; const IS_VIRTUAL_KEY: bool = true; const IS_REAL_KEY: bool = false; @@ -88,22 +84,19 @@ impl TryFrom<&str> for KeyboardShortcutInput { type Error = anyhow::Error; fn try_from(key: &str) -> std::result::Result { - const SHIFT_KEY: u16 = 0x10; - const CONTROL_KEY: u16 = 0x11; - const ALT_KEY: u16 = 0x12; - const LEFT_WINDOWS_KEY: u16 = 0x5B; - + // not modifier key + if key.len() == 1 { + let input = build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?); + return Ok(KeyboardShortcutInput(input)); + } // the modifier keys are using the Up keypress variant because the user has already // pressed those keys in order to trigger the feature. - let input = match key { - SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY), - CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY), - ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY), - LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY), - _ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?), - }; - - Ok(KeyboardShortcutInput(input)) + if let Some(numeric_modifier_key) = get_numeric_modifier_key(key) { + let input = build_virtual_key_input(InputKeyPress::Up, numeric_modifier_key); + Ok(KeyboardShortcutInput(input)) + } else { + Err(anyhow!("Unsupported modifier key: {key}")) + } } } @@ -278,7 +271,7 @@ mod tests { #[test] #[serial] fn keyboard_shortcut_conversion_succeeds() { - let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "B"]; + let keyboard_shortcut = ["Control", "Alt", "B"]; let _: Vec = keyboard_shortcut .iter() .map(|s| KeyboardShortcutInput::try_from(*s)) @@ -290,7 +283,19 @@ mod tests { #[serial] #[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '1'"] fn keyboard_shortcut_conversion_fails_invalid_key() { - let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "1"]; + let keyboard_shortcut = ["Control", "Alt", "1"]; + let _: Vec = keyboard_shortcut + .iter() + .map(|s| KeyboardShortcutInput::try_from(*s)) + .try_collect() + .unwrap(); + } + + #[test] + #[serial] + #[should_panic(expected = "Unsupported modifier key: Shift")] + fn keyboard_shortcut_conversion_fails_with_shift() { + let keyboard_shortcut = ["Control", "Shift", "B"]; let _: Vec = keyboard_shortcut .iter() .map(|s| KeyboardShortcutInput::try_from(*s)) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/Cargo.toml index ff641731661..5cc457809f2 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_import_helper/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_import_helper/Cargo.toml @@ -9,19 +9,19 @@ publish.workspace = true [target.'cfg(target_os = "windows")'.dependencies] aes-gcm = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } chacha20poly1305 = { workspace = true } chromium_importer = { path = "../chromium_importer" } clap = { version = "=4.5.53", features = ["derive"] } scopeguard = { workspace = true } sysinfo = { workspace = true } -windows = { workspace = true, features = [ - "Win32_System_Pipes", -] } -anyhow = { workspace = true } -base64 = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } +windows = { workspace = true, features = [ + "Win32_System_Pipes", +] } [build-dependencies] embed-resource = "=3.0.6" diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index 54a6dba8326..b20aa7e5af8 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -20,47 +20,79 @@ fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true }); const args = process.argv.slice(2); // Get arguments passed to the script const mode = args.includes("--release") ? "release" : "debug"; +const isRelease = mode === "release"; const targetArg = args.find(arg => arg.startsWith("--target=")); const target = targetArg ? targetArg.split("=")[1] : null; let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; +/** + * Execute a command. + * @param {string} bin Executable to run. + * @param {string[]} args Arguments for executable. + * @param {string} [workingDirectory] Path to working directory, relative to the script directory. Defaults to the script directory. + * @param {string} [useShell] Whether to use a shell to execute the command. Defaults to false. + */ +function runCommand(bin, args, workingDirectory = "", useShell = false) { + const options = { stdio: 'inherit', cwd: path.resolve(__dirname, workingDirectory), shell: useShell } + console.debug("Running command:", bin, args, options) + child_process.execFileSync(bin, args, options) +} + function buildNapiModule(target, release = true) { - const targetArg = target ? `--target ${target}` : ""; + const targetArg = target ? `--target=${target}` : ""; const releaseArg = release ? "--release" : ""; - child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); + const crossCompileArg = effectivePlatform(target) !== process.platform ? "--cross-compile" : ""; + runCommand("npm", ["run", "build", "--", crossCompileArg, releaseArg, targetArg].filter(s => s != ''), "./napi", true); +} + +/** + * Build a Rust binary with Cargo. + * + * If {@link target} is specified, cross-compilation helpers are used to build if necessary, and the resulting + * binary is copied to the `dist` folder. + * @param {string} bin Name of cargo binary package in `desktop_native` workspace. + * @param {string?} target Rust compiler target, e.g. `aarch64-pc-windows-msvc`. + * @param {boolean} release Whether to build in release mode. + */ +function cargoBuild(bin, target, release) { + const targetArg = target ? `--target=${target}` : ""; + const releaseArg = release ? "--release" : ""; + const args = ["build", "--bin", bin, releaseArg, targetArg] + // Use cross-compilation helper if necessary + if (effectivePlatform(target) === "win32" && process.platform !== "win32") { + args.unshift("xwin") + } + runCommand("cargo", args.filter(s => s != '')) + + // Infer the architecture and platform if not passed explicitly + let nodeArch, platform; + if (target) { + nodeArch = rustTargetsMap[target].nodeArch; + platform = rustTargetsMap[target].platform; + } + else { + nodeArch = process.arch; + platform = process.platform; + } + + // Copy the resulting binary to the dist folder + const profileFolder = isRelease ? "release" : "debug"; + const ext = platform === "win32" ? ".exe" : ""; + const src = path.join(__dirname, "target", target ? target : "", profileFolder, `${bin}${ext}`) + const dst = path.join(__dirname, "dist", `${bin}.${platform}-${nodeArch}${ext}`) + console.log(`Copying ${src} to ${dst}`); + fs.copyFileSync(src, dst); } function buildProxyBin(target, release = true) { - const targetArg = target ? `--target ${target}` : ""; - const releaseArg = release ? "--release" : ""; - child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); - - if (target) { - // Copy the resulting binary to the dist folder - const targetFolder = release ? "release" : "debug"; - const ext = process.platform === "win32" ? ".exe" : ""; - const nodeArch = rustTargetsMap[target].nodeArch; - fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); - } + cargoBuild("desktop_proxy", target, release) } function buildImporterBinaries(target, release = true) { // These binaries are only built for Windows, so we can skip them on other platforms - if (process.platform !== "win32") { - return; - } - - const bin = "bitwarden_chromium_import_helper"; - const targetArg = target ? `--target ${target}` : ""; - const releaseArg = release ? "--release" : ""; - child_process.execSync(`cargo build --bin ${bin} ${releaseArg} ${targetArg}`); - - if (target) { - // Copy the resulting binary to the dist folder - const targetFolder = release ? "release" : "debug"; - const nodeArch = rustTargetsMap[target].nodeArch; - fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `${bin}.exe`), path.join(__dirname, "dist", `${bin}.${process.platform}-${nodeArch}.exe`)); + if (effectivePlatform(target) == "win32") { + cargoBuild("bitwarden_chromium_import_helper", target, release) } } @@ -69,17 +101,29 @@ function buildProcessIsolation() { return; } - child_process.execSync(`cargo build --release`, { - stdio: 'inherit', - cwd: path.join(__dirname, "process_isolation") - }); + runCommand("cargo", ["build", "--package", "process_isolation", "--release"]); console.log("Copying process isolation library to dist folder"); fs.copyFileSync(path.join(__dirname, "target", "release", "libprocess_isolation.so"), path.join(__dirname, "dist", `libprocess_isolation.so`)); } function installTarget(target) { - child_process.execSync(`rustup target add ${target}`, { stdio: 'inherit', cwd: __dirname }); + runCommand("rustup", ["target", "add", target]); + // Install cargo-xwin for cross-platform builds targeting Windows + if (target.includes('windows') && process.platform !== 'win32') { + runCommand("cargo", ["install", "--version", "0.20.2", "--locked", "cargo-xwin"]); + // install tools needed for packaging Appx, only supported on macOS for now. + if (process.platform === "darwin") { + runCommand("brew", ["install", "iinuwa/msix-packaging-tap/msix-packaging", "osslsigncode"]); + } + } +} + +function effectivePlatform(target) { + if (target) { + return rustTargetsMap[target].platform + } + return process.platform } if (!crossPlatform && !target) { @@ -94,9 +138,9 @@ if (!crossPlatform && !target) { if (target) { console.log(`Building for target: ${target} in ${mode} mode`); installTarget(target); - buildNapiModule(target, mode === "release"); - buildProxyBin(target, mode === "release"); - buildImporterBinaries(false, mode === "release"); + buildNapiModule(target, isRelease); + buildProxyBin(target, isRelease); + buildImporterBinaries(target, isRelease); buildProcessIsolation(); return; } @@ -113,8 +157,8 @@ if (process.platform === "linux") { platformTargets.forEach(([target, _]) => { installTarget(target); - buildNapiModule(target, mode === "release"); - buildProxyBin(target, mode === "release"); - buildImporterBinaries(target, mode === "release"); + buildNapiModule(target, isRelease); + buildProxyBin(target, isRelease); + buildImporterBinaries(target, isRelease); buildProcessIsolation(); }); diff --git a/apps/desktop/desktop_native/chromium_importer/Cargo.toml b/apps/desktop/desktop_native/chromium_importer/Cargo.toml index 9e9a9e0fee8..9bb1c0b87f2 100644 --- a/apps/desktop/desktop_native/chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/chromium_importer/Cargo.toml @@ -16,6 +16,12 @@ rusqlite = { version = "=0.37.0", features = ["bundled"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +[target.'cfg(target_os = "linux")'.dependencies] +cbc = { workspace = true, features = ["alloc"] } +oo7 = { workspace = true } +pbkdf2 = "=0.12.2" +sha1 = "=0.10.6" + [target.'cfg(target_os = "macos")'.dependencies] cbc = { workspace = true, features = ["alloc"] } pbkdf2 = "=0.12.2" @@ -25,20 +31,14 @@ sha1 = "=0.10.6" [target.'cfg(target_os = "windows")'.dependencies] aes-gcm = { workspace = true } base64 = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +verifysign = "=0.2.4" windows = { workspace = true, features = [ "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", ] } -verifysign = "=0.2.4" -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true } - -[target.'cfg(target_os = "linux")'.dependencies] -cbc = { workspace = true, features = ["alloc"] } -oo7 = { workspace = true } -pbkdf2 = "=0.12.2" -sha1 = "=0.10.6" [lints] workspace = true diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index dc9246f55c6..aa5d564c9e5 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -13,7 +13,7 @@ default = [ "dep:security-framework", "dep:security-framework-sys", "dep:zbus", - "dep:zbus_polkit" + "dep:zbus_polkit", ] manual_test = [] @@ -46,6 +46,23 @@ tracing = { workspace = true } typenum = { workspace = true } zeroizing-alloc = { workspace = true } +[target.'cfg(target_os = "linux")'.dependencies] +ashpd = { workspace = true } +homedir = { workspace = true } +libc = { workspace = true } +linux-keyutils = { workspace = true } +oo7 = { workspace = true } +zbus = { workspace = true, optional = true } +zbus_polkit = { workspace = true, optional = true } + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = { workspace = true, optional = true } +desktop_objc = { path = "../objc" } +homedir = { workspace = true } +secmem-proc = { workspace = true } +security-framework = { workspace = true, optional = true } +security-framework-sys = { workspace = true, optional = true } + [target.'cfg(windows)'.dependencies] pin-project = { workspace = true } scopeguard = { workspace = true } @@ -68,22 +85,5 @@ windows = { workspace = true, features = [ ], optional = true } windows-future = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { workspace = true, optional = true } -homedir = { workspace = true } -secmem-proc = { workspace = true } -security-framework = { workspace = true, optional = true } -security-framework-sys = { workspace = true, optional = true } -desktop_objc = { path = "../objc" } - -[target.'cfg(target_os = "linux")'.dependencies] -ashpd = { workspace = true } -homedir = { workspace = true } -libc = { workspace = true } -linux-keyutils = { workspace = true } -oo7 = { workspace = true } -zbus = { workspace = true, optional = true } -zbus_polkit = { workspace = true, optional = true } - [lints] workspace = true diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 16cf778b575..8ba64618ffa 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -226,7 +226,7 @@ impl BitwardenDesktopAgent { keystore.0.write().expect("RwLock is not poisoned").clear(); self.needs_unlock - .store(false, std::sync::atomic::Ordering::Relaxed); + .store(true, std::sync::atomic::Ordering::Relaxed); for (key, name, cipher_id) in new_keys.iter() { match parse_key_safe(key) { @@ -307,87 +307,3 @@ fn parse_key_safe(pem: &str) -> Result Err(anyhow::Error::msg(format!("Failed to parse key: {e}"))), } } - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_agent() -> ( - BitwardenDesktopAgent, - tokio::sync::mpsc::Receiver, - tokio::sync::broadcast::Sender<(u32, bool)>, - ) { - let (tx, rx) = tokio::sync::mpsc::channel(10); - let (response_tx, response_rx) = tokio::sync::broadcast::channel(10); - let agent = BitwardenDesktopAgent::new(tx, Arc::new(Mutex::new(response_rx))); - (agent, rx, response_tx) - } - - const TEST_ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trwAAAIhqmvSbapr0 -mwAAAAtzc2gtZWQyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trw -AAAEAHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVfBWT4eZYRMQiH8lf5lorReWDnFn1A -h+OIoPZCzns+OFFjjm2vAAAAAAECAwQF ------END OPENSSH PRIVATE KEY-----"; - - #[tokio::test] - async fn test_needs_unlock_initial_state() { - let (agent, _rx, _response_tx) = create_test_agent(); - - // Initially, needs_unlock should be true - assert!(agent - .needs_unlock - .load(std::sync::atomic::Ordering::Relaxed)); - } - - #[tokio::test] - async fn test_needs_unlock_after_set_keys() { - let (mut agent, _rx, _response_tx) = create_test_agent(); - agent - .is_running - .store(true, std::sync::atomic::Ordering::Relaxed); - - // Set keys should set needs_unlock to false - let keys = vec![( - TEST_ED25519_KEY.to_string(), - "test_key".to_string(), - "cipher_id".to_string(), - )]; - - agent.set_keys(keys).unwrap(); - - assert!(!agent - .needs_unlock - .load(std::sync::atomic::Ordering::Relaxed)); - } - - #[tokio::test] - async fn test_needs_unlock_after_clear_keys() { - let (mut agent, _rx, _response_tx) = create_test_agent(); - agent - .is_running - .store(true, std::sync::atomic::Ordering::Relaxed); - - // Set keys first - let keys = vec![( - TEST_ED25519_KEY.to_string(), - "test_key".to_string(), - "cipher_id".to_string(), - )]; - agent.set_keys(keys).unwrap(); - - // Verify needs_unlock is false - assert!(!agent - .needs_unlock - .load(std::sync::atomic::Ordering::Relaxed)); - - // Clear keys should set needs_unlock back to true - agent.clear_keys().unwrap(); - - // Verify needs_unlock is true - assert!(agent - .needs_unlock - .load(std::sync::atomic::Ordering::Relaxed)); - } -} diff --git a/apps/desktop/desktop_native/deny.toml b/apps/desktop/desktop_native/deny.toml index 7d7a126f694..66b80e9984c 100644 --- a/apps/desktop/desktop_native/deny.toml +++ b/apps/desktop/desktop_native/deny.toml @@ -1,9 +1,10 @@ # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] +# Allow unmaintained crates in transient deps but not direct +unmaintained = "workspace" ignore = [ # Vulnerability in `rsa` crate: https://rustsec.org/advisories/RUSTSEC-2023-0071.html { id = "RUSTSEC-2023-0071", reason = "There is no fix available yet." }, - { id = "RUSTSEC-2024-0436", reason = "paste crate is unmaintained."} ] # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index 8a34460268a..d73bd2fa049 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -5,14 +5,14 @@ license = { workspace = true } version = { workspace = true } publish = { workspace = true } -[[bin]] -name = "uniffi-bindgen" -path = "uniffi-bindgen.rs" - [lib] crate-type = ["staticlib", "cdylib"] bench = false +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" + [dependencies] uniffi = { workspace = true, features = ["cli"] } @@ -23,8 +23,8 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true } tracing-oslog = "=0.3.0" +tracing-subscriber = { workspace = true } [build-dependencies] uniffi = { workspace = true, features = ["build"] } diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml index dd808537c28..1edf30ac996 100644 --- a/apps/desktop/desktop_native/objc/Cargo.toml +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -14,7 +14,7 @@ tokio = { workspace = true } tracing = { workspace = true } [target.'cfg(target_os = "macos")'.build-dependencies] -cc = "=1.2.49" +cc = "=1.2.51" glob = "=0.3.3" [lints] diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml index 17c834325a4..9fd873d868e 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/Cargo.toml @@ -6,6 +6,7 @@ license = { workspace = true } publish = { workspace = true } [target.'cfg(windows)'.dependencies] +hex = { workspace = true } windows = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", @@ -13,7 +14,6 @@ windows = { workspace = true, features = [ "Win32_System_LibraryLoader", ] } windows-core = { workspace = true } -hex = { workspace = true } [lints] workspace = true diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 0c95c7f01a6..3e1ca673c3c 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -1,4 +1,6 @@ { + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "extraMetadata": { "name": "bitwarden-beta" }, @@ -13,14 +15,15 @@ }, "afterSign": "scripts/after-sign.js", "afterPack": "scripts/after-pack.js", - "asarUnpack": ["**/*.node"], + "beforePack": "scripts/before-pack.js", "files": [ - "**/*", - "!**/node_modules/@bitwarden/desktop-napi/**/*", - "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" + "!node_modules/@bitwarden/desktop-napi/scripts", + "!node_modules/@bitwarden/desktop-napi/src", + "!node_modules/@bitwarden/desktop-napi/Cargo.toml", + "!node_modules/@bitwarden/desktop-napi/build.rs", + "!node_modules/@bitwarden/desktop-napi/package.json" ], - "electronVersion": "36.8.1", + "electronVersion": "37.7.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -34,11 +37,11 @@ }, "extraFiles": [ { - "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "from": "desktop_native/dist/desktop_proxy.win32-${arch}.exe", "to": "desktop_proxy.exe" }, { - "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "from": "desktop_native/dist/bitwarden_chromium_import_helper.win32-${arch}.exe", "to": "bitwarden_chromium_import_helper.exe" } ] @@ -58,9 +61,10 @@ "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", + "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index a4e1c44dc5b..83bd2921551 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -1,4 +1,6 @@ { + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "extraMetadata": { "name": "bitwarden" }, @@ -13,12 +15,13 @@ }, "afterSign": "scripts/after-sign.js", "afterPack": "scripts/after-pack.js", - "asarUnpack": ["**/*.node"], + "beforePack": "scripts/before-pack.js", "files": [ - "**/*", - "!**/node_modules/@bitwarden/desktop-napi/**/*", - "**/node_modules/@bitwarden/desktop-napi/index.js", - "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" + "!node_modules/@bitwarden/desktop-napi/scripts", + "!node_modules/@bitwarden/desktop-napi/src", + "!node_modules/@bitwarden/desktop-napi/Cargo.toml", + "!node_modules/@bitwarden/desktop-napi/build.rs", + "!node_modules/@bitwarden/desktop-napi/package.json" ], "electronVersion": "39.2.6", "generateUpdatesFilesForAllChannels": true, @@ -94,11 +97,11 @@ }, "extraFiles": [ { - "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", + "from": "desktop_native/dist/desktop_proxy.win32-${arch}.exe", "to": "desktop_proxy.exe" }, { - "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "from": "desktop_native/dist/bitwarden_chromium_import_helper.win32-${arch}.exe", "to": "bitwarden_chromium_import_helper.exe" } ] @@ -172,9 +175,10 @@ "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", + "publisher": "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US", "publisherDisplayName": "Bitwarden Inc", "languages": [ "en-US", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 97ab8585a69..ad20e7c0e69 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -29,7 +29,7 @@ "build:macos-extension:mas": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas", "build:macos-extension:masdev": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js mas-dev", "build:main": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name main", - "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main", + "build:main:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main --watch", "build:renderer": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name renderer", "build:renderer:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer", @@ -40,16 +40,16 @@ "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", "pack:lin:flatpak": "flatpak-builder --repo=../../.flatpak-repo ../../.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ../../.flatpak-repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", - "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && tar -czvf ./dist/bitwarden_desktop_x64.tar.gz -C ./dist/linux-unpacked/ .", - "pack:lin:arm64": "npm run clean:dist && electron-builder --linux --arm64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", + "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && cp ./resources/com.bitwarden.desktop.desktop ./dist/linux-unpacked/resources && cp -r ./resources/icons ./dist/linux-unpacked/resources && tar -czvf ./dist/bitwarden_desktop_x64.tar.gz -C ./dist/linux-unpacked/ .", + "pack:lin:arm64": "npm run clean:dist && electron-builder --linux --arm64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && cp ./resources/com.bitwarden.desktop.desktop ./dist/linux-arm64-unpacked/resources && cp -r ./resources/icons ./dist/linux-arm64-unpacked/resources && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never", "pack:mac:masdev": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never", "pack:local:mac": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"", - "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", - "pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", + "pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", + "pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never", "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", @@ -62,7 +62,7 @@ "publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always", "publish:mac": "npm run build && npm run clean:dist && electron-builder --mac -p always", "publish:mac:mas": "npm run dist:mac:mas && npm run upload:mas", - "publish:win": "npm run build && npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p always -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"", + "publish:win": "npm run build && npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p always", "publish:win:dev": "npm run build:dev && npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p always", "upload:mas": "xcrun altool --upload-app --type osx --file \"$(find ./dist/mas-universal/Bitwarden*.pkg)\" --apiKey $APP_STORE_CONNECT_AUTH_KEY --apiIssuer $APP_STORE_CONNECT_TEAM_ISSUER", "test": "jest", diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index 5fc42f31ac3..34378ee092b 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -6,9 +6,12 @@ const path = require("path"); const { flipFuses, FuseVersion, FuseV1Options } = require("@electron/fuses"); const builder = require("electron-builder"); const fse = require("fs-extra"); - exports.default = run; +/** + * + * @param {builder.AfterPackContext} context + */ async function run(context) { console.log("## After pack"); // console.log(context); diff --git a/apps/desktop/scripts/appx-cross-build.ps1 b/apps/desktop/scripts/appx-cross-build.ps1 new file mode 100755 index 00000000000..62619d5ea37 --- /dev/null +++ b/apps/desktop/scripts/appx-cross-build.ps1 @@ -0,0 +1,226 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS +Script to build, package and sign the Bitwarden desktop client as a Windows Appx +package. + +.DESCRIPTION +This script provides cross-platform support for packaging and signing the +Bitwarden desktop client as a Windows Appx package. + +Currently, only macOS -> Windows Appx is supported, but Linux -> Windows Appx +could be added in the future by providing Linux binaries for the msix-packaging +project. + +.NOTES +The reason this script exists is because electron-builder does not currently +support cross-platform Appx packaging without proprietary tools (Parallels +Windows VM). This script uses third-party tools (makemsix from msix-packaging +and osslsigncode) to package and sign the Appx. + +The signing certificate must have the same subject as the publisher name. This +can be generated on the Windows target using PowerShell 5.1 and copied to the +host, or directly on the host with OpenSSL. + +Using Windows PowerShell 5.1: +```powershell +$publisher = "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US" +$certificate = New-SelfSignedCertificate -Type Custom -KeyUsage DigitalSignature -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") -Subject $publisher -FriendlyName "Bitwarden Developer Signing Certificate" +$password = Read-Host -AsSecureString +Export-PfxCertificate -cert "Cert:\CurrentUser\My\${$certificate.Thumbprint}" -FilePath "C:\path/to/pfx" -Password $password +``` + +Using OpenSSL: +```sh +subject="jurisdictionCountryName=US/jurisdictionStateOrProvinceName=Delaware/businessCategory=Private Organization/serialNumber=7654941, C=US, ST=California, L=Santa Barbara, O=Bitwarden Inc., CN=Bitwarden Inc." +keyfile="/tmp/mysigning.rsa.pem" +certfile="/tmp/mysigning.cert.pem" +p12file="/tmp/mysigning.p12" +openssl req -x509 -keyout "$keyfile" -out "$certfile" -subj "$subject" \ + -newkey rsa:2048 -days 3650 -nodes \ + -addext 'keyUsage=critical,digitalSignature' \ + -addext 'extendedKeyUsage=critical,codeSigning' \ + -addext 'basicConstraints=critical,CA:FALSE' +openssl pkcs12 -inkey "$keyfile" -in "$certfile" -export -out "$p12file" +rm $keyfile +``` + +.EXAMPLE +./scripts/cross-build.ps1 -Architecture arm64 -CertificatePath ~/Development/code-signing.pfx -CertificatePassword (Read-Host -AsSecureString) -Release -Beta + +Reads the signing certificate password from user input, then builds, packages +and signs the Appx. + +Alternatively, you can specify the CERTIFICATE_PASSWORD environment variable. +#> +param( + [Parameter(Mandatory=$true)] + [ValidateSet("X64", "ARM64")]$Architecture, + [string] + # Path to PKCS12 certificate file. If not specified, the Appx will not be signed. + $CertificatePath, + [SecureString] + # Password for PKCS12 certificate. Alternatively, may be specified in + # CERTIFICATE_PASSWORD environment variable. If not specified, the Appx will + # not be signed. + $CertificatePassword, + [Switch] + # Whether to build the Beta version of the app. + $Beta=$false, + [Switch] + # Whether to build in release mode. + $Release=$false +) +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $true +$startTime = Get-Date +$originalLocation = Get-Location +if (!(Get-Command makemsix -ErrorAction SilentlyContinue)) { + Write-Error "The `makemsix` tool from the msix-packaging project is required to construct Appx package." + Write-Error "On macOS, you can install with Homebrew:" + Write-Error " brew install iinuwa/msix-packaging-tap/msix-packaging" + Exit 1 +} + +if (!(Get-Command osslsigncode -ErrorAction SilentlyContinue)) { + Write-Error "The `osslsigncode` tool is required to sign the Appx package." + Write-Error "On macOS, you can install with Homebrew:" + Write-Error " brew install osslsigncode" + Exit 1 +} + +if (!(Get-Command cargo-xwin -ErrorAction SilentlyContinue)) { + Write-Error "The `cargo-xwin` tool is required to cross-compile Windows native code." + Write-Error "You can install with cargo:" + Write-Error " cargo install --version 0.20.2 --locked cargo-xwin" + Exit 1 +} + +try { + +# Resolve certificate file before we change directories. +$CertificateFile = Get-Item $CertificatePath -ErrorAction SilentlyContinue + +cd $PSScriptRoot/.. + +if ($Beta) { + $electronConfigFile = Get-Item "./electron-builder.beta.json" +} +else { + $electronConfigFile = Get-Item "./electron-builder.json" +} + +$builderConfig = Get-Content $electronConfigFile | ConvertFrom-Json +$packageConfig = Get-Content package.json | ConvertFrom-Json +$manifestTemplate = Get-Content $builderConfig.appx.customManifestPath + +$srcDir = Get-Location +$assetsDir = Get-Item $builderConfig.directories.buildResources +$buildDir = Get-Item $builderConfig.directories.app +$outDir = Join-Path (Get-Location) ($builderConfig.directories.output ?? "dist") + +if ($Release) { + $buildConfiguration = "--release" +} +$arch = "$Architecture".ToLower() +$ext = "appx" +$version = Get-Date -Format "yyyy.M.d.1HHmm" +$productName = $builderConfig.productName +$artifactName = "${productName}-$($packageConfig.version)-${arch}.$ext" + +Write-Host "Building native code" +$rustTarget = switch ($arch) { + x64 { "x86_64-pc-windows-msvc" } + arm64 { "aarch64-pc-windows-msvc" } + default { + Write-Error "Unsupported architecture: $Architecture. Supported architectures are x64 and arm64" + Exit(1) + } +} +npm run build-native -- cross-platform $buildConfiguration "--target=$rustTarget" + +Write-Host "Building Javascript code" +if ($Release) { + npm run build +} +else { + npm run build:dev +} + +Write-Host "Cleaning output folder" +Remove-Item -Recurse -Force $outDir -ErrorAction Ignore + +Write-Host "Packaging Electron executable" +& npx electron-builder --config $electronConfigFile --publish never --dir --win --$arch + +cd $outDir +New-Item -Type Directory (Join-Path $outDir "appx") + +Write-Host "Building Appx directory structure" +$appxDir = (Join-Path $outDir appx/app) +if ($arch -eq "x64") { + Move-Item (Join-Path $outDir "win-unpacked") $appxDir +} +else { + Move-Item (Join-Path $outDir "win-${arch}-unpacked") $appxDir +} + +Write-Host "Copying Assets" +New-Item -Type Directory (Join-Path $outDir appx/assets) +Copy-Item $srcDir/resources/appx/* $outDir/appx/assets/ + +Write-Host "Building Appx manifest" +$translationMap = @{ + 'arch' = $arch + 'applicationId' = $builderConfig.appx.applicationId + 'displayName' = $productName + 'executable' = "app\${productName}.exe" + 'publisher' = $builderConfig.appx.publisher + 'publisherDisplayName' = $builderConfig.appx.publisherDisplayName + 'version' = $version +} + +$manifest = $manifestTemplate +$translationMap.Keys | ForEach-Object { + $manifest = $manifest.Replace("`${$_}", $translationMap[$_]) +} +$manifest | Out-File appx/AppxManifest.xml +$unsignedArtifactpath = [System.IO.Path]::GetFileNameWithoutExtension($artifactName) + "-unsigned.$ext" +Write-Host "Creating unsigned Appx" +makemsix pack -d appx -p $unsignedArtifactpath + +$outfile = Join-Path $outDir $unsignedArtifactPath +if ($null -eq $CertificatePath) { + Write-Warning "No Certificate specified. Not signing Appx." +} +elseif ($null -eq $CertificatePassword -and $null -eq $env:CERTIFICATE_PASSWORD) { + Write-Warning "No certificate password specified in CertificatePassword argument nor CERTIFICATE_PASSWORD environment variable. Not signing Appx." +} +else { + $cert = $CertificateFile + $pw = $null + if ($null -ne $CertificatePassword) { + $pw = ConvertFrom-SecureString -SecureString $CertificatePassword -AsPlainText + } else { + $pw = $env:CERTIFICATE_PASSWORD + } + $unsigned = $outfile + $outfile = (Join-Path $outDir $artifactName) + Write-Host "Signing $artifactName with $cert" + osslsigncode sign ` + -pkcs12 "$cert" ` + -pass "$pw" ` + -in $unsigned ` + -out $outfile + Remove-Item $unsigned +} + +$endTime = Get-Date +$elapsed = $endTime - $startTime +Write-Host "Successfully packaged $(Get-Item $outfile)" +Write-Host ("Finished at $($endTime.ToString('HH:mm:ss')) in $($elapsed.ToString('mm')) minutes and $($elapsed.ToString('ss')).$($elapsed.ToString('fff')) seconds") +} +finally { + Set-Location -Path $originalLocation +} diff --git a/apps/desktop/scripts/before-pack.js b/apps/desktop/scripts/before-pack.js new file mode 100644 index 00000000000..ca9bf924b2d --- /dev/null +++ b/apps/desktop/scripts/before-pack.js @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ +/** @import { BeforePackContext } from 'app-builder-lib' */ +exports.default = run; + +/** + * @param {BeforePackContext} context + */ +async function run(context) { + console.log("## before pack"); + console.log("Stripping .node files that don't belong to this platform..."); + removeExtraNodeFiles(context); +} + +/** + * Removes Node files for platforms besides the current platform being packaged. + * + * @param {BeforePackContext} context + */ +function removeExtraNodeFiles(context) { + // When doing cross-platform builds, due to electron-builder limitiations, + // .node files for other platforms may be generated and unpacked, so we + // remove them manually here before signing and distributing. + const packagerPlatform = context.packager.platform.nodeName; + const platforms = ["darwin", "linux", "win32"]; + const fileFilter = context.packager.info._configuration.files[0].filter; + for (const platform of platforms) { + if (platform != packagerPlatform) { + fileFilter.push(`!node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-*.node`); + } + } +} diff --git a/apps/desktop/sign.js b/apps/desktop/sign.js index 6a42666c46f..f115e9b8097 100644 --- a/apps/desktop/sign.js +++ b/apps/desktop/sign.js @@ -1,22 +1,60 @@ /* eslint-disable @typescript-eslint/no-require-imports, no-console */ +const child_process = require("child_process"); exports.default = async function (configuration) { - if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") { + const ext = configuration.path.split(".").at(-1); + if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && ["exe", "appx"].includes(ext)) { console.log(`[*] Signing file: ${configuration.path}`); - require("child_process").execSync( - `azuresigntool sign -v ` + - `-kvu ${process.env.SIGNING_VAULT_URL} ` + - `-kvi ${process.env.SIGNING_CLIENT_ID} ` + - `-kvt ${process.env.SIGNING_TENANT_ID} ` + - `-kvs ${process.env.SIGNING_CLIENT_SECRET} ` + - `-kvc ${process.env.SIGNING_CERT_NAME} ` + - `-fd ${configuration.hash} ` + - `-du ${configuration.site} ` + - `-tr http://timestamp.digicert.com ` + - `"${configuration.path}"`, + child_process.execFileSync( + "azuresigntool", + // prettier-ignore + [ + "sign", + "-v", + "-kvu", process.env.SIGNING_VAULT_URL, + "-kvi", process.env.SIGNING_CLIENT_ID, + "-kvt", process.env.SIGNING_TENANT_ID, + "-kvs", process.env.SIGNING_CLIENT_SECRET, + "-kvc", process.env.SIGNING_CERT_NAME, + "-fd", configuration.hash, + "-du", configuration.site, + "-tr", "http://timestamp.digicert.com", + configuration.path, + ], { stdio: "inherit", }, ); + } else if (process.env.ELECTRON_BUILDER_SIGN_CERT && ["exe", "appx"].includes(ext)) { + console.log(`[*] Signing file: ${configuration.path}`); + if (process.platform !== "win32") { + console.warn( + "Signing Windows executables on non-Windows platforms is not supported. Not signing.", + ); + return; + } + const certFile = process.env.ELECTRON_BUILDER_SIGN_CERT; + const certPw = process.env.ELECTRON_BUILDER_SIGN_CERT_PW; + if (!certPw) { + throw new Error( + "The certificate file password must be set in ELECTRON_BUILDER_SIGN_CERT_PW in order to sign files.", + ); + } + try { + child_process.execFileSync( + "signtool.exe", + ["sign", "/fd", "SHA256", "/a", "/f", certFile, "/p", certPw, configuration.path], + { + stdio: "inherit", + }, + ); + console.info(`Signed ${configuration.path} successfully.`); + } catch (error) { + throw new Error( + `Failed to sign ${configuration.path}: ${error.message}\n` + + `Check that ELECTRON_BUILDER_SIGN_CERT points to a valid PKCS12 file ` + + `and ELECTRON_BUILDER_SIGN_CERT_PW is correct.`, + ); + } } }; diff --git a/apps/desktop/src/app/accounts/settings.component.spec.ts b/apps/desktop/src/app/accounts/settings.component.spec.ts index d518ac29aa4..bffa06d2654 100644 --- a/apps/desktop/src/app/accounts/settings.component.spec.ts +++ b/apps/desktop/src/app/accounts/settings.component.spec.ts @@ -188,7 +188,7 @@ describe("SettingsComponent", () => { pinServiceAbstraction.isPinSet.mockResolvedValue(false); policyService.policiesByType$.mockReturnValue(of([null])); desktopAutotypeService.autotypeEnabledUserSetting$ = of(false); - desktopAutotypeService.autotypeKeyboardShortcut$ = of(["Control", "Shift", "B"]); + desktopAutotypeService.autotypeKeyboardShortcut$ = of(["Control", "Alt", "B"]); billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); configService.getFeatureFlag$.mockReturnValue(of(false)); }); diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 6077afa8c12..e9b6dfdc9e5 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -114,6 +114,8 @@ const routes: Routes = [ authGuard, canAccessFeature(FeatureFlag.DesktopUiMigrationMilestone1, false, "new-vault", false), ], + // Needed to ensure feature flag changes are picked up on account switching + runGuardsAndResolvers: "always", }, { path: "send", @@ -361,6 +363,7 @@ const routes: Routes = [ { path: "new-sends", component: SendV2Component, + data: { pageTitle: { key: "send" } } satisfies RouteDataProperties, }, ], }, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index d75702ee8b8..01eb8c728e5 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -31,6 +31,7 @@ import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; import { + AuthRequestServiceAbstraction, DESKTOP_SSO_CALLBACK, LockService, LogoutReason, @@ -40,11 +41,13 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -151,6 +154,8 @@ export class AppComponent implements OnInit, OnDestroy { private isIdle = false; private activeUserId: UserId = null; private activeSimpleDialog: DialogRef = null; + private processingPendingAuthRequests = false; + private shouldRerunAuthRequestProcessing = false; private destroy$ = new Subject(); @@ -200,6 +205,9 @@ export class AppComponent implements OnInit, OnDestroy { private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy, private readonly lockService: LockService, private premiumUpgradePromptService: PremiumUpgradePromptService, + private pendingAuthRequestsState: PendingAuthRequestsStateService, + private authRequestService: AuthRequestServiceAbstraction, + private authRequestAnsweringService: AuthRequestAnsweringService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -212,6 +220,8 @@ export class AppComponent implements OnInit, OnDestroy { this.activeUserId = account?.id; }); + this.authRequestAnsweringService.setupUnlockListenersForProcessingAuthRequests(this.destroy$); + this.ngZone.runOutsideAngular(() => { setTimeout(async () => { await this.updateAppMenu(); @@ -482,9 +492,8 @@ export class AppComponent implements OnInit, OnDestroy { this.loading = true; await this.syncService.fullSync(false); this.loading = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["vault"]); + // Force reload to ensure route guards are activated + await this.router.navigate(["vault"], { onSameUrlNavigation: "reload" }); } this.messagingService.send("finishSwitchAccount"); break; @@ -499,13 +508,31 @@ export class AppComponent implements OnInit, OnDestroy { await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle); break; case "openLoginApproval": - if (message.notificationId != null) { - this.dialogService.closeAll(); - const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { - notificationId: message.notificationId, - }); - await firstValueFrom(dialogRef.closed); + if (this.processingPendingAuthRequests) { + // If an "openLoginApproval" message is received while we are currently processing other + // auth requests, then set a flag so we remember to process that new auth request + this.shouldRerunAuthRequestProcessing = true; + return; } + + /** + * This do/while loop allows us to: + * - a) call processPendingAuthRequests() once on "openLoginApproval" + * - b) remember to re-call processPendingAuthRequests() if another "openLoginApproval" was + * received while we were processing the original auth requests + */ + do { + this.shouldRerunAuthRequestProcessing = false; + + try { + await this.processPendingAuthRequests(); + } catch (error) { + this.logService.error(`Error processing pending auth requests: ${error}`); + this.shouldRerunAuthRequestProcessing = false; // Reset flag to prevent infinite loop on persistent errors + } + // If an "openLoginApproval" message was received while processPendingAuthRequests() was running, then + // shouldRerunAuthRequestProcessing will have been set to true + } while (this.shouldRerunAuthRequestProcessing); break; case "redrawMenu": await this.updateAppMenu(); @@ -887,4 +914,39 @@ export class AppComponent implements OnInit, OnDestroy { DeleteAccountComponent.open(this.dialogService); } + + private async processPendingAuthRequests() { + this.processingPendingAuthRequests = true; + + try { + // Always query server for all pending requests and open a dialog for each + const pendingList = await firstValueFrom(this.authRequestService.getPendingAuthRequests$()); + + if (Array.isArray(pendingList) && pendingList.length > 0) { + const respondedIds = new Set(); + + for (const req of pendingList) { + if (req?.id == null) { + continue; + } + + const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { + notificationId: req.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + respondedIds.add(req.id); + + if (respondedIds.size === pendingList.length && this.activeUserId != null) { + await this.pendingAuthRequestsState.clear(this.activeUserId); + } + } + } + } + } finally { + this.processingPendingAuthRequests = false; + } + } } diff --git a/apps/desktop/src/app/layout/desktop-layout.component.html b/apps/desktop/src/app/layout/desktop-layout.component.html index 1717b29acd1..cb969f573fc 100644 --- a/apps/desktop/src/app/layout/desktop-layout.component.html +++ b/apps/desktop/src/app/layout/desktop-layout.component.html @@ -1,9 +1,13 @@ - + - - + + + + + + diff --git a/apps/desktop/src/app/layout/desktop-layout.component.spec.ts b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts index 253444232e5..c838f47a06c 100644 --- a/apps/desktop/src/app/layout/desktop-layout.component.spec.ts +++ b/apps/desktop/src/app/layout/desktop-layout.component.spec.ts @@ -5,9 +5,10 @@ import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { FakeGlobalStateProvider } from "@bitwarden/common/spec"; -import { NavigationModule } from "@bitwarden/components"; +import { DialogService, NavigationModule } from "@bitwarden/components"; import { GlobalStateProvider } from "@bitwarden/state"; +import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component"; import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component"; import { DesktopLayoutComponent } from "./desktop-layout.component"; @@ -20,6 +21,13 @@ import { DesktopLayoutComponent } from "./desktop-layout.component"; }) class MockSendFiltersNavComponent {} +@Component({ + selector: "app-vault-filter", + template: "", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class MockVaultFiltersNavComponent {} + Object.defineProperty(window, "matchMedia", { writable: true, value: jest.fn().mockImplementation((query) => ({ @@ -52,11 +60,15 @@ describe("DesktopLayoutComponent", () => { provide: GlobalStateProvider, useValue: fakeGlobalStateProvider, }, + { + provide: DialogService, + useValue: mock(), + }, ], }) .overrideComponent(DesktopLayoutComponent, { - remove: { imports: [SendFiltersNavComponent] }, - add: { imports: [MockSendFiltersNavComponent] }, + remove: { imports: [SendFiltersNavComponent, VaultFilterComponent] }, + add: { imports: [MockSendFiltersNavComponent, MockVaultFiltersNavComponent] }, }) .compileComponents(); @@ -89,4 +101,11 @@ describe("DesktopLayoutComponent", () => { expect(sendFiltersNav).toBeTruthy(); }); + + it("renders vault filters navigation component", () => { + const compiled = fixture.nativeElement; + const vaultFiltersNav = compiled.querySelector("app-vault-filter"); + + expect(vaultFiltersNav).toBeTruthy(); + }); }); diff --git a/apps/desktop/src/app/layout/desktop-layout.component.ts b/apps/desktop/src/app/layout/desktop-layout.component.ts index 0ee7065fba8..8d6ced2eb7d 100644 --- a/apps/desktop/src/app/layout/desktop-layout.component.ts +++ b/apps/desktop/src/app/layout/desktop-layout.component.ts @@ -1,10 +1,14 @@ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; import { PasswordManagerLogo } from "@bitwarden/assets/svg"; -import { LayoutComponent, NavigationModule } from "@bitwarden/components"; +import { DialogService, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultFilterComponent } from "../../vault/app/vault-v3/vault-filter/vault-filter.component"; +import { ExportDesktopComponent } from "../tools/export/export-desktop.component"; +import { CredentialGeneratorComponent } from "../tools/generator/credential-generator.component"; +import { ImportDesktopComponent } from "../tools/import/import-desktop.component"; import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component"; import { DesktopSideNavComponent } from "./desktop-side-nav.component"; @@ -19,10 +23,25 @@ import { DesktopSideNavComponent } from "./desktop-side-nav.component"; LayoutComponent, NavigationModule, DesktopSideNavComponent, + VaultFilterComponent, SendFiltersNavComponent, ], templateUrl: "./desktop-layout.component.html", }) export class DesktopLayoutComponent { + private dialogService = inject(DialogService); + protected readonly logo = PasswordManagerLogo; + + protected openGenerator() { + this.dialogService.open(CredentialGeneratorComponent); + } + + protected openImport() { + this.dialogService.open(ImportDesktopComponent); + } + + protected openExport() { + this.dialogService.open(ExportDesktopComponent); + } } diff --git a/apps/desktop/src/app/layout/header/desktop-header.component.html b/apps/desktop/src/app/layout/header/desktop-header.component.html new file mode 100644 index 00000000000..ae578312535 --- /dev/null +++ b/apps/desktop/src/app/layout/header/desktop-header.component.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/app/layout/header/desktop-header.component.spec.ts b/apps/desktop/src/app/layout/header/desktop-header.component.spec.ts new file mode 100644 index 00000000000..8d3db198887 --- /dev/null +++ b/apps/desktop/src/app/layout/header/desktop-header.component.spec.ts @@ -0,0 +1,122 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { HeaderComponent } from "@bitwarden/components"; + +import { DesktopHeaderComponent } from "./desktop-header.component"; + +describe("DesktopHeaderComponent", () => { + let component: DesktopHeaderComponent; + let fixture: ComponentFixture; + let mockI18nService: ReturnType>; + let mockActivatedRoute: { data: any }; + + beforeEach(async () => { + mockI18nService = mock(); + mockI18nService.t.mockImplementation((key: string) => `translated_${key}`); + + mockActivatedRoute = { + data: of({}), + }; + + await TestBed.configureTestingModule({ + imports: [DesktopHeaderComponent, HeaderComponent], + providers: [ + { + provide: I18nService, + useValue: mockI18nService, + }, + { + provide: ActivatedRoute, + useValue: mockActivatedRoute, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DesktopHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates component", () => { + expect(component).toBeTruthy(); + }); + + it("renders bit-header component", () => { + const compiled = fixture.nativeElement; + const headerElement = compiled.querySelector("bit-header"); + + expect(headerElement).toBeTruthy(); + }); + + describe("title resolution", () => { + it("uses title input when provided", () => { + fixture.componentRef.setInput("title", "Direct Title"); + fixture.detectChanges(); + + expect(component["resolvedTitle"]()).toBe("Direct Title"); + }); + + it("uses route data titleId when no direct title provided", () => { + mockActivatedRoute.data = of({ + pageTitle: { key: "sends" }, + }); + + fixture = TestBed.createComponent(DesktopHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(mockI18nService.t).toHaveBeenCalledWith("sends"); + expect(component["resolvedTitle"]()).toBe("translated_sends"); + }); + + it("returns empty string when no title or route data provided", () => { + mockActivatedRoute.data = of({}); + + fixture = TestBed.createComponent(DesktopHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component["resolvedTitle"]()).toBe(""); + }); + + it("prioritizes direct title over route data", () => { + mockActivatedRoute.data = of({ + pageTitle: { key: "sends" }, + }); + + fixture = TestBed.createComponent(DesktopHeaderComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput("title", "Override Title"); + fixture.detectChanges(); + + expect(component["resolvedTitle"]()).toBe("Override Title"); + }); + }); + + describe("icon input", () => { + it("accepts icon input", () => { + fixture.componentRef.setInput("icon", "bwi-send"); + fixture.detectChanges(); + + expect(component.icon()).toBe("bwi-send"); + }); + + it("defaults to undefined when no icon provided", () => { + expect(component.icon()).toBeUndefined(); + }); + }); + + describe("content projection", () => { + it("wraps bit-header component for slot pass-through", () => { + const compiled = fixture.nativeElement; + const bitHeader = compiled.querySelector("bit-header"); + + // Verify bit-header exists and can receive projected content + expect(bitHeader).toBeTruthy(); + }); + }); +}); diff --git a/apps/desktop/src/app/layout/header/desktop-header.component.ts b/apps/desktop/src/app/layout/header/desktop-header.component.ts new file mode 100644 index 00000000000..5a837f1ff5a --- /dev/null +++ b/apps/desktop/src/app/layout/header/desktop-header.component.ts @@ -0,0 +1,47 @@ +import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { ActivatedRoute } from "@angular/router"; +import { map } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { HeaderComponent, BannerModule } from "@bitwarden/components"; + +@Component({ + selector: "app-header", + templateUrl: "./desktop-header.component.html", + imports: [BannerModule, HeaderComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DesktopHeaderComponent { + private route = inject(ActivatedRoute); + private i18nService = inject(I18nService); + + /** + * Title to display in header (takes precedence over route data) + */ + readonly title = input(); + + /** + * Icon to show before the title + */ + readonly icon = input(); + + private readonly routeData = toSignal( + this.route.data.pipe( + map((params) => ({ + titleId: params["pageTitle"]?.["key"] as string | undefined, + })), + ), + { initialValue: { titleId: undefined } }, + ); + + protected readonly resolvedTitle = computed(() => { + const directTitle = this.title(); + if (directTitle) { + return directTitle; + } + + const titleId = this.routeData().titleId; + return titleId ? this.i18nService.t(titleId) : ""; + }); +} diff --git a/apps/desktop/src/app/layout/header/index.ts b/apps/desktop/src/app/layout/header/index.ts new file mode 100644 index 00000000000..793d90f81e5 --- /dev/null +++ b/apps/desktop/src/app/layout/header/index.ts @@ -0,0 +1 @@ +export { DesktopHeaderComponent } from "./desktop-header.component"; diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 9be8e0cbb85..86ef8f18d4c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,11 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { APP_INITIALIZER, NgModule } from "@angular/core"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { Subject, merge } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { LoginApprovalDialogComponentServiceAbstraction } from "@bitwarden/angular/auth/login-approval"; +import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -37,6 +36,7 @@ import { } from "@bitwarden/auth/common"; 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 as PolicyServiceAbstraction, InternalPolicyService, @@ -45,6 +45,7 @@ import { AccountService, AccountService as AccountServiceAbstraction, } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService, AuthService as AuthServiceAbstraction, @@ -52,6 +53,7 @@ import { import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ClientType } from "@bitwarden/common/enums"; @@ -61,7 +63,10 @@ import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout"; @@ -103,6 +108,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; import { GeneratorServicesModule } from "@bitwarden/generator-components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -118,10 +124,18 @@ import { SessionTimeoutSettingsComponentService, } from "@bitwarden/key-management-ui"; import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; -import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault"; +import { + DefaultSshImportPromptService, + SshImportPromptService, + VaultFilterServiceAbstraction, + VaultFilterService, + RoutedVaultFilterService, + RoutedVaultFilterBridgeService, + VAULT_FILTER_BASE_ROUTE, +} from "@bitwarden/vault"; -import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; +import { DesktopAuthRequestAnsweringService } from "../../auth/services/auth-request-answering/desktop-auth-request-answering.service"; import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; @@ -470,11 +484,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSsoComponentService, deps: [], }), - safeProvider({ - provide: LoginApprovalDialogComponentServiceAbstraction, - useClass: DesktopLoginApprovalDialogComponentService, - deps: [I18nServiceAbstraction], - }), safeProvider({ provide: SshImportPromptService, useClass: DefaultSshImportPromptService, @@ -510,6 +519,47 @@ const safeProviders: SafeProvider[] = [ useClass: SessionTimeoutSettingsComponentService, deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyServiceAbstraction], }), + safeProvider({ + provide: VaultFilterServiceAbstraction, + useClass: VaultFilterService, + deps: [ + OrganizationService, + FolderService, + CipherServiceAbstraction, + PolicyServiceAbstraction, + I18nServiceAbstraction, + StateProvider, + CollectionService, + AccountServiceAbstraction, + ], + }), + safeProvider({ + provide: VAULT_FILTER_BASE_ROUTE, + useValue: "/new-vault", + }), + safeProvider({ + provide: RoutedVaultFilterService, + useClass: RoutedVaultFilterService, + deps: [ActivatedRoute], + }), + safeProvider({ + provide: RoutedVaultFilterBridgeService, + useClass: RoutedVaultFilterBridgeService, + deps: [Router, RoutedVaultFilterService, VaultFilterServiceAbstraction], + }), + safeProvider({ + provide: AuthRequestAnsweringService, + useClass: DesktopAuthRequestAnsweringService, + deps: [ + AccountServiceAbstraction, + AuthService, + MasterPasswordServiceAbstraction, + MessagingServiceAbstraction, + PendingAuthRequestsStateService, + I18nServiceAbstraction, + LogService, + ], + }), ]; @NgModule({ diff --git a/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.spec.ts index ab881e5b57b..f22b94974d1 100644 --- a/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.spec.ts +++ b/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.spec.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { FakeGlobalStateProvider } from "@bitwarden/common/spec"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { NavigationModule } from "@bitwarden/components"; import { SendListFiltersService } from "@bitwarden/send-ui"; import { GlobalStateProvider } from "@bitwarden/state"; diff --git a/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.ts b/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.ts index 28004f475e5..0dfdc1ee7c5 100644 --- a/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-filters-nav.component.ts @@ -4,7 +4,7 @@ import { toSignal } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import { filter, map, startWith } from "rxjs"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { NavigationModule } from "@bitwarden/components"; import { SendListFiltersService } from "@bitwarden/send-ui"; import { I18nPipe } from "@bitwarden/ui-common"; diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.html b/apps/desktop/src/app/tools/send-v2/send-v2.component.html index 20cac15138a..eda740fa721 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.html +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.html @@ -1,110 +1,25 @@ -
-
-
-
- -
-
- - - -

{{ "noItemsInList" | i18n }}

-
-
- -
-
- - -
+ + + @if (!disableSend()) { + + } + + + + + diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts index 8657f3e375e..a73a0534ff9 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts @@ -2,66 +2,90 @@ // @ts-strict-ignore import { ChangeDetectorRef } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { FormBuilder } from "@angular/forms"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { ActivatedRoute } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { of } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +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"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { SendListFiltersService } from "@bitwarden/send-ui"; - -import * as utils from "../../../utils"; -import { SearchBarService } from "../../layout/search/search-bar.service"; -import { AddEditComponent } from "../send/add-edit.component"; +import { + SendItemsService, + SendListFiltersService, + DefaultSendFormConfigService, + SendAddEditDialogComponent, + SendFormConfig, +} from "@bitwarden/send-ui"; import { SendV2Component } from "./send-v2.component"; -// Mock the invokeMenu utility function -jest.mock("../../../utils", () => ({ - invokeMenu: jest.fn(), -})); - describe("SendV2Component", () => { let component: SendV2Component; let fixture: ComponentFixture; let sendService: MockProxy; - let searchBarService: MockProxy; - let broadcasterService: MockProxy; let accountService: MockProxy; let policyService: MockProxy; - let sendListFiltersService: SendListFiltersService; + let sendItemsService: MockProxy; + let sendListFiltersService: MockProxy; let changeDetectorRef: MockProxy; + let sendFormConfigService: MockProxy; + let dialogService: MockProxy; + let environmentService: MockProxy; + let platformUtilsService: MockProxy; + let sendApiService: MockProxy; + let toastService: MockProxy; + let i18nService: MockProxy; beforeEach(async () => { sendService = mock(); - searchBarService = mock(); - broadcasterService = mock(); accountService = mock(); policyService = mock(); changeDetectorRef = mock(); + sendFormConfigService = mock(); + dialogService = mock(); + environmentService = mock(); + platformUtilsService = mock(); + sendApiService = mock(); + toastService = mock(); + i18nService = mock(); - // Create real SendListFiltersService with mocked dependencies - const formBuilder = new FormBuilder(); - const i18nService = mock(); + // Setup environmentService mock + environmentService.getEnvironment.mockResolvedValue({ + getSendUrl: () => "https://send.bitwarden.com/#/", + } as any); + + // Setup i18nService mock i18nService.t.mockImplementation((key: string) => key); - sendListFiltersService = new SendListFiltersService(i18nService, formBuilder); + + // Mock SendItemsService with all required observables + sendItemsService = mock(); + sendItemsService.filteredAndSortedSends$ = of([]); + sendItemsService.loading$ = of(false); + sendItemsService.emptyList$ = of(false); + sendItemsService.noFilteredResults$ = of(false); + sendItemsService.latestSearchText$ = of(""); + + // Mock SendListFiltersService + sendListFiltersService = mock(); // Mock sendViews$ observable sendService.sendViews$ = of([]); - searchBarService.searchText$ = new BehaviorSubject(""); - // Mock activeAccount$ observable for parent class ngOnInit + // Mock activeAccount$ observable accountService.activeAccount$ = of({ id: "test-user-id" } as any); policyService.policyAppliesToUser$ = jest.fn().mockReturnValue(of(false)); @@ -72,23 +96,45 @@ describe("SendV2Component", () => { await TestBed.configureTestingModule({ imports: [SendV2Component], providers: [ + provideNoopAnimations(), { provide: SendService, useValue: sendService }, - { provide: I18nService, useValue: mock() }, - { provide: PlatformUtilsService, useValue: mock() }, - { provide: EnvironmentService, useValue: mock() }, - { provide: BroadcasterService, useValue: broadcasterService }, + { provide: I18nService, useValue: i18nService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + { provide: EnvironmentService, useValue: environmentService }, { provide: SearchService, useValue: mockSearchService }, { provide: PolicyService, useValue: policyService }, - { provide: SearchBarService, useValue: searchBarService }, { provide: LogService, useValue: mock() }, - { provide: SendApiService, useValue: mock() }, - { provide: DialogService, useValue: mock() }, - { provide: ToastService, useValue: mock() }, + { provide: SendApiService, useValue: sendApiService }, + { provide: DialogService, useValue: dialogService }, + { provide: DefaultSendFormConfigService, useValue: sendFormConfigService }, + { provide: ToastService, useValue: toastService }, { provide: AccountService, useValue: accountService }, + { provide: SendItemsService, useValue: sendItemsService }, { provide: SendListFiltersService, useValue: sendListFiltersService }, { provide: ChangeDetectorRef, useValue: changeDetectorRef }, + { + provide: BillingAccountProfileStateService, + useValue: mock(), + }, + { provide: MessagingService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { + provide: ActivatedRoute, + useValue: { + data: of({}), + }, + }, ], - }).compileComponents(); + }) + .overrideComponent(SendV2Component, { + set: { + providers: [ + { provide: DefaultSendFormConfigService, useValue: sendFormConfigService }, + { provide: PremiumUpgradePromptService, useValue: mock() }, + ], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(SendV2Component); component = fixture.componentInstance; @@ -98,289 +144,110 @@ describe("SendV2Component", () => { expect(component).toBeTruthy(); }); - it("initializes with correct default action", () => { - expect(component.action).toBe(""); - }); - - it("subscribes to broadcaster service on init", async () => { - await component.ngOnInit(); - expect(broadcasterService.subscribe).toHaveBeenCalledWith( - "SendV2Component", - expect.any(Function), - ); - }); - - it("unsubscribes from broadcaster service on destroy", () => { - component.ngOnDestroy(); - expect(broadcasterService.unsubscribe).toHaveBeenCalledWith("SendV2Component"); - }); - - it("enables search bar on init", async () => { - await component.ngOnInit(); - expect(searchBarService.setEnabled).toHaveBeenCalledWith(true); - }); - - it("disables search bar on destroy", () => { - component.ngOnDestroy(); - expect(searchBarService.setEnabled).toHaveBeenCalledWith(false); - }); - describe("addSend", () => { - it("sets action to Add", async () => { - await component.addSend(); - expect(component.action).toBe("add"); + beforeEach(() => { + jest.clearAllMocks(); }); - it("calls resetAndLoad on addEditComponent when component exists", async () => { - const mockAddEdit = mock(); - component.addEditComponent = mockAddEdit; + it("opens dialog with correct config for Text send", async () => { + const mockConfig = { mode: "add", sendType: SendType.Text } as SendFormConfig; + const mockDialogRef = { closed: of(true) }; - await component.addSend(); + sendFormConfigService.buildConfig.mockResolvedValue(mockConfig); + const openDrawerSpy = jest + .spyOn(SendAddEditDialogComponent, "openDrawer") + .mockReturnValue(mockDialogRef as any); - expect(mockAddEdit.resetAndLoad).toHaveBeenCalled(); + await component["addSend"](SendType.Text); + + expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith( + "add", + undefined, + SendType.Text, + ); + expect(openDrawerSpy).toHaveBeenCalled(); + expect(openDrawerSpy.mock.calls[0][1]).toEqual({ + formConfig: mockConfig, + }); }); - it("does not throw when addEditComponent is null", async () => { - component.addEditComponent = null; - await expect(component.addSend()).resolves.not.toThrow(); - }); - }); + it("opens dialog with correct config for File send", async () => { + const mockConfig = { mode: "add", sendType: SendType.File } as SendFormConfig; + const mockDialogRef = { closed: of(true) }; - describe("cancel", () => { - it("resets action to None", () => { - component.action = "edit"; - component.sendId = "test-id"; + sendFormConfigService.buildConfig.mockResolvedValue(mockConfig); + const openDrawerSpy = jest + .spyOn(SendAddEditDialogComponent, "openDrawer") + .mockReturnValue(mockDialogRef as any); - component.cancel(new SendView()); + await component["addSend"](SendType.File); - expect(component.action).toBe(""); - expect(component.sendId).toBeNull(); - }); - }); - - describe("deletedSend", () => { - it("refreshes the list and resets action and sendId", async () => { - component.action = "edit"; - component.sendId = "test-id"; - jest.spyOn(component, "refresh").mockResolvedValue(); - - const mockSend = new SendView(); - await component.deletedSend(mockSend); - - expect(component.refresh).toHaveBeenCalled(); - expect(component.action).toBe(""); - expect(component.sendId).toBeNull(); - }); - }); - - describe("savedSend", () => { - it("refreshes the list and selects the saved send", async () => { - jest.spyOn(component, "refresh").mockResolvedValue(); - jest.spyOn(component, "selectSend").mockResolvedValue(); - - const mockSend = new SendView(); - mockSend.id = "saved-send-id"; - - await component.savedSend(mockSend); - - expect(component.refresh).toHaveBeenCalled(); - expect(component.selectSend).toHaveBeenCalledWith("saved-send-id"); + expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith( + "add", + undefined, + SendType.File, + ); + expect(openDrawerSpy).toHaveBeenCalled(); + expect(openDrawerSpy.mock.calls[0][1]).toEqual({ + formConfig: mockConfig, + }); }); }); describe("selectSend", () => { - it("sets action to Edit and updates sendId", async () => { - await component.selectSend("new-send-id"); - - expect(component.action).toBe("edit"); - expect(component.sendId).toBe("new-send-id"); - }); - - it("updates addEditComponent when it exists", async () => { - const mockAddEdit = mock(); - component.addEditComponent = mockAddEdit; - - await component.selectSend("test-send-id"); - - expect(mockAddEdit.sendId).toBe("test-send-id"); - expect(mockAddEdit.refresh).toHaveBeenCalled(); - }); - - it("does not reload if same send is already selected in edit mode", async () => { - const mockAddEdit = mock(); - component.addEditComponent = mockAddEdit; - component.sendId = "same-id"; - component.action = "edit"; - - await component.selectSend("same-id"); - - expect(mockAddEdit.refresh).not.toHaveBeenCalled(); - }); - - it("reloads if selecting different send", async () => { - const mockAddEdit = mock(); - component.addEditComponent = mockAddEdit; - component.sendId = "old-id"; - component.action = "edit"; - - await component.selectSend("new-id"); - - expect(mockAddEdit.refresh).toHaveBeenCalled(); - }); - }); - - describe("selectedSendType", () => { - it("returns the type of the currently selected send", () => { - const mockSend1 = new SendView(); - mockSend1.id = "send-1"; - mockSend1.type = SendType.Text; - - const mockSend2 = new SendView(); - mockSend2.id = "send-2"; - mockSend2.type = SendType.File; - - component.sends = [mockSend1, mockSend2]; - component.sendId = "send-2"; - - expect(component.selectedSendType).toBe(SendType.File); - }); - - it("returns undefined when no send is selected", () => { - component.sends = []; - component.sendId = "non-existent"; - - expect(component.selectedSendType).toBeUndefined(); - }); - - it("returns undefined when sendId is null", () => { - const mockSend = new SendView(); - mockSend.id = "send-1"; - mockSend.type = SendType.Text; - - component.sends = [mockSend]; - component.sendId = null; - - expect(component.selectedSendType).toBeUndefined(); - }); - }); - - describe("viewSendMenu", () => { - let mockSend: SendView; - beforeEach(() => { - mockSend = new SendView(); - mockSend.id = "test-send"; - mockSend.name = "Test Send"; jest.clearAllMocks(); }); - it("creates menu with copy link option", () => { - jest.spyOn(component, "copy").mockResolvedValue(); + it("opens dialog with correct config for editing send", async () => { + const mockConfig = { mode: "edit", sendId: "test-send-id" } as SendFormConfig; + const mockDialogRef = { closed: of(true) }; - component.viewSendMenu(mockSend); + sendFormConfigService.buildConfig.mockResolvedValue(mockConfig); + const openDrawerSpy = jest + .spyOn(SendAddEditDialogComponent, "openDrawer") + .mockReturnValue(mockDialogRef as any); - expect(utils.invokeMenu).toHaveBeenCalled(); - const menuItems = (utils.invokeMenu as jest.Mock).mock.calls[0][0]; - expect(menuItems.length).toBeGreaterThanOrEqual(2); // At minimum: copy link + delete - }); + await component["selectSend"]("test-send-id"); - it("includes remove password option when send has password and is not disabled", () => { - mockSend.password = "test-password"; - mockSend.disabled = false; - jest.spyOn(component, "removePassword").mockResolvedValue(true); - - component.viewSendMenu(mockSend); - - expect(utils.invokeMenu).toHaveBeenCalled(); - const menuItems = (utils.invokeMenu as jest.Mock).mock.calls[0][0]; - expect(menuItems.length).toBe(3); // copy link + remove password + delete - }); - - it("excludes remove password option when send has no password", () => { - mockSend.password = null; - mockSend.disabled = false; - - component.viewSendMenu(mockSend); - - expect(utils.invokeMenu).toHaveBeenCalled(); - const menuItems = (utils.invokeMenu as jest.Mock).mock.calls[0][0]; - expect(menuItems.length).toBe(2); // copy link + delete (no remove password) - }); - - it("excludes remove password option when send is disabled", () => { - mockSend.password = "test-password"; - mockSend.disabled = true; - - component.viewSendMenu(mockSend); - - expect(utils.invokeMenu).toHaveBeenCalled(); - const menuItems = (utils.invokeMenu as jest.Mock).mock.calls[0][0]; - expect(menuItems.length).toBe(2); // copy link + delete (no remove password) - }); - - it("always includes delete option", () => { - jest.spyOn(component, "delete").mockResolvedValue(true); - jest.spyOn(component, "deletedSend").mockResolvedValue(); - - component.viewSendMenu(mockSend); - - expect(utils.invokeMenu).toHaveBeenCalled(); - const menuItems = (utils.invokeMenu as jest.Mock).mock.calls[0][0]; - // Delete is always the last item in the menu - expect(menuItems.length).toBeGreaterThan(0); - expect(menuItems[menuItems.length - 1]).toHaveProperty("label"); - expect(menuItems[menuItems.length - 1]).toHaveProperty("click"); + expect(sendFormConfigService.buildConfig).toHaveBeenCalledWith("edit", "test-send-id"); + expect(openDrawerSpy).toHaveBeenCalled(); + expect(openDrawerSpy.mock.calls[0][1]).toEqual({ + formConfig: mockConfig, + }); }); }); - describe("search bar subscription", () => { - it("updates searchText when search bar text changes", () => { - const searchSubject = new BehaviorSubject("initial"); - searchBarService.searchText$ = searchSubject; + describe("onEditSend", () => { + it("selects the send for editing", async () => { + jest.spyOn(component as any, "selectSend").mockResolvedValue(undefined); + const mockSend = new SendView(); + mockSend.id = "edit-send-id"; - // Create new component to trigger constructor subscription - fixture = TestBed.createComponent(SendV2Component); - component = fixture.componentInstance; + await component["onEditSend"](mockSend); - searchSubject.next("new search text"); - - expect(component.searchText).toBe("new search text"); + expect(component["selectSend"]).toHaveBeenCalledWith("edit-send-id"); }); }); - describe("load", () => { - it("sets loading states correctly", async () => { - jest.spyOn(component, "search").mockResolvedValue(); + describe("onCopySend", () => { + it("copies send link to clipboard and shows success toast", async () => { + const mockSend = { + accessId: "test-access-id", + urlB64Key: "test-key", + } as SendView; - expect(component.loaded).toBeFalsy(); + await component["onCopySend"](mockSend); - await component.load(); - - expect(component.loading).toBe(false); - expect(component.loaded).toBe(true); - }); - - it("sets up sendViews$ subscription", async () => { - const mockSends = [new SendView(), new SendView()]; - sendService.sendViews$ = of(mockSends); - jest.spyOn(component, "search").mockResolvedValue(); - - await component.load(); - - // Give observable time to emit - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(component.sends).toEqual(mockSends); - }); - - it("calls onSuccessfulLoad when it is set", async () => { - jest.spyOn(component, "search").mockResolvedValue(); - const mockCallback = jest.fn().mockResolvedValue(undefined); - component.onSuccessfulLoad = mockCallback; - - await component.load(); - - expect(mockCallback).toHaveBeenCalled(); + expect(environmentService.getEnvironment).toHaveBeenCalled(); + expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith( + "https://send.bitwarden.com/#/test-access-id/test-key", + ); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: expect.any(String), + }); }); }); }); 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 eb0856b76af..7fab0cb6702 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,261 +1,203 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; -import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormsModule } from "@angular/forms"; -import { mergeMap, Subscription } from "rxjs"; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + effect, + inject, +} from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { combineLatest, map, switchMap, lastValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; -import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { SendListFiltersService } from "@bitwarden/send-ui"; +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 { + NewSendDropdownV2Component, + SendItemsService, + SendListComponent, + SendListState, + SendAddEditDialogComponent, + DefaultSendFormConfigService, +} from "@bitwarden/send-ui"; -import { invokeMenu, RendererMenuItem } from "../../../utils"; -import { SearchBarService } from "../../layout/search/search-bar.service"; -import { AddEditComponent } from "../send/add-edit.component"; +import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; +import { DesktopHeaderComponent } from "../../layout/header"; -const Action = Object.freeze({ - /** No action is currently active. */ - None: "", - /** The user is adding a new Send. */ - Add: "add", - /** The user is editing an existing Send. */ - Edit: "edit", -} as const); - -type Action = (typeof Action)[keyof typeof Action]; - -const BroadcasterSubscriptionId = "SendV2Component"; - -// 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-send-v2", - imports: [CommonModule, JslibModule, FormsModule, AddEditComponent], + imports: [ + JslibModule, + ButtonModule, + SendListComponent, + NewSendDropdownV2Component, + DesktopHeaderComponent, + ], + providers: [ + DefaultSendFormConfigService, + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + ], templateUrl: "./send-v2.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SendV2Component extends BaseSendComponent implements OnInit, OnDestroy { - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild(AddEditComponent) addEditComponent: AddEditComponent; +export class SendV2Component { + private sendFormConfigService = inject(DefaultSendFormConfigService); + private sendItemsService = inject(SendItemsService); + private policyService = inject(PolicyService); + private accountService = inject(AccountService); + private i18nService = inject(I18nService); + private platformUtilsService = inject(PlatformUtilsService); + private environmentService = inject(EnvironmentService); + private sendApiService = inject(SendApiService); + private dialogService = inject(DialogService); + private toastService = inject(ToastService); + private logService = inject(LogService); + private cdr = inject(ChangeDetectorRef); - // The ID of the currently selected Send item being viewed or edited - sendId: string; + protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, { + initialValue: [], + }); - // Tracks the current UI state: viewing list (None), adding new Send (Add), or editing existing Send (Edit) - action: Action = Action.None; + protected readonly loading = toSignal(this.sendItemsService.loading$, { initialValue: true }); - // Subscription for sendViews$ cleanup - private sendViewsSubscription: Subscription; + protected readonly currentSearchText = toSignal(this.sendItemsService.latestSearchText$, { + initialValue: "", + }); - constructor( - sendService: SendService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - environmentService: EnvironmentService, - private broadcasterService: BroadcasterService, - ngZone: NgZone, - searchService: SearchService, - policyService: PolicyService, - private searchBarService: SearchBarService, - logService: LogService, - sendApiService: SendApiService, - dialogService: DialogService, - toastService: ToastService, - accountService: AccountService, - private cdr: ChangeDetectorRef, - private sendListFiltersService: SendListFiltersService, - ) { - super( - sendService, - i18nService, - platformUtilsService, - environmentService, - ngZone, - searchService, - policyService, - logService, - sendApiService, - dialogService, - toastService, - accountService, - ); + protected readonly disableSend = toSignal( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + ), + { initialValue: false }, + ); - // Listen to search bar changes and update the Send list filter - this.searchBarService.searchText$.pipe(takeUntilDestroyed()).subscribe((searchText) => { - this.searchText = searchText; - this.searchTextChanged(); - }); - - // Listen to filter changes from sidebar navigation - this.sendListFiltersService.filterForm.valueChanges - .pipe(takeUntilDestroyed()) - .subscribe((filters) => { - this.applySendTypeFilter(filters); - }); - } - - // Initialize the component: enable search bar, subscribe to sync events, and load Send items - async ngOnInit() { - this.searchBarService.setEnabled(true); - this.searchBarService.setPlaceholderText(this.i18nService.t("searchSends")); - - await super.ngOnInit(); - - // Read current filter synchronously to avoid race condition on navigation - const currentFilter = this.sendListFiltersService.filterForm.value; - this.applySendTypeFilter(currentFilter); - - // Listen for sync completion events to refresh the Send list - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - await this.load(); - break; + protected readonly listState = toSignal( + combineLatest([ + this.sendItemsService.emptyList$, + this.sendItemsService.noFilteredResults$, + ]).pipe( + map(([emptyList, noFilteredResults]): SendListState | null => { + if (emptyList) { + return SendListState.Empty; } - }); + if (noFilteredResults) { + return SendListState.NoResults; + } + return null; + }), + ), + { initialValue: null }, + ); + + constructor() { + // WORKAROUND: Force change detection when data updates + // This is needed because SendSearchComponent (shared lib) hasn't migrated to OnPush yet + // and doesn't trigger CD properly when search/add operations complete + // TODO: Remove this once SendSearchComponent migrates to OnPush (tracked in CL-764) + effect(() => { + this.filteredSends(); + this.cdr.markForCheck(); }); - await this.load(); } - // Apply send type filter to display: centralized logic for initial load and filter changes - private applySendTypeFilter(filters: Partial<{ sendType: SendType | null }>): void { - if (filters.sendType === null || filters.sendType === undefined) { - this.selectAll(); - } else { - this.selectType(filters.sendType); - } + protected async addSend(type: SendType): Promise { + const formConfig = await this.sendFormConfigService.buildConfig("add", undefined, type); + + const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + formConfig, + }); + + await lastValueFrom(dialogRef.closed); } - // Clean up subscriptions and disable search bar when component is destroyed - ngOnDestroy() { - this.sendViewsSubscription?.unsubscribe(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.searchBarService.setEnabled(false); + protected async selectSend(sendId: SendId): Promise { + const formConfig = await this.sendFormConfigService.buildConfig("edit", sendId); + + const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { + formConfig, + }); + + await lastValueFrom(dialogRef.closed); } - // Load Send items from the service and display them in the list. - // Subscribes to sendViews$ observable to get updates when Sends change. - // Manually triggers change detection to ensure UI updates immediately. - // Note: The filter parameter is ignored in this implementation for desktop-specific behavior. - async load(filter: (send: SendView) => boolean = null) { - this.loading = true; - - // Recreate subscription on each load (required for sync refresh) - // Manual cleanup in ngOnDestroy is intentional - load() is called multiple times - this.sendViewsSubscription?.unsubscribe(); - - this.sendViewsSubscription = this.sendService.sendViews$ - .pipe( - mergeMap(async (sends) => { - this.sends = sends; - await this.search(null); - // Trigger change detection after data updates - this.cdr.detectChanges(); - }), - ) - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - .subscribe(); - if (this.onSuccessfulLoad != null) { - await this.onSuccessfulLoad(); - } - this.loading = false; - this.loaded = true; + protected async onEditSend(send: SendView): Promise { + await this.selectSend(send.id as SendId); } - // Open the add Send form to create a new Send item - async addSend() { - this.action = Action.Add; - if (this.addEditComponent != null) { - await this.addEditComponent.resetAndLoad(); - } + protected async onCopySend(send: SendView): Promise { + const env = await this.environmentService.getEnvironment(); + const link = env.getSendUrl() + send.accessId + "/" + send.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")), + }); } - // Close the add/edit form and return to the list view - cancel(s: SendView) { - this.action = Action.None; - this.sendId = null; - } - - // Handle when a Send is deleted: refresh the list and close the edit form - async deletedSend(s: SendView) { - await this.refresh(); - this.action = Action.None; - this.sendId = null; - } - - // Handle when a Send is saved: refresh the list and re-select the saved Send - async savedSend(s: SendView) { - await this.refresh(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.selectSend(s.id); - } - - // Select a Send from the list and open it in the edit form. - // If the same Send is already selected and in edit mode, do nothing to avoid unnecessary reloads. - async selectSend(sendId: string) { - if (sendId === this.sendId && this.action === Action.Edit) { + protected async onRemovePassword(send: SendView): Promise { + if (this.disableSend()) { return; } - this.action = Action.Edit; - this.sendId = sendId; - if (this.addEditComponent != null) { - this.addEditComponent.sendId = sendId; - await this.addEditComponent.refresh(); - } - } - // Get the type (text or file) of the currently selected Send for the edit form - get selectedSendType() { - return this.sends.find((s) => s.id === this.sendId)?.type; - } - - // Show the right-click context menu for a Send with options to copy link, remove password, or delete - viewSendMenu(send: SendView) { - const menu: RendererMenuItem[] = []; - menu.push({ - label: this.i18nService.t("copyLink"), - click: () => this.copy(send), + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removePassword" }, + content: { key: "removePasswordConfirmation" }, + type: "warning", }); - if (send.password && !send.disabled) { - menu.push({ - label: this.i18nService.t("removePassword"), - click: async () => { - await this.removePassword(send); - if (this.sendId === send.id) { - this.sendId = null; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.selectSend(send.id); - } - }, + + if (!confirmed) { + return; + } + + try { + await this.sendApiService.removePassword(send.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedPassword"), }); + } catch (e) { + this.logService.error(e); } - menu.push({ - label: this.i18nService.t("delete"), - click: async () => { - await this.delete(send); - await this.deletedSend(send); - }, + } + + protected async onDeleteSend(send: SendView): Promise { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteSend" }, + content: { key: "deleteSendConfirmation" }, + type: "warning", }); - invokeMenu(menu); + if (!confirmed) { + return; + } + + await this.sendApiService.delete(send.id); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedSend"), + }); } } diff --git a/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.spec.ts b/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.spec.ts deleted file mode 100644 index 2ae584d7e7f..00000000000 --- a/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { mock, MockProxy } from "jest-mock-extended"; -import { Subject } from "rxjs"; - -import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval"; -import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; - -import { DesktopLoginApprovalDialogComponentService } from "./desktop-login-approval-dialog-component.service"; - -describe("DesktopLoginApprovalDialogComponentService", () => { - let service: DesktopLoginApprovalDialogComponentService; - let i18nService: MockProxy; - let originalIpc: any; - - beforeEach(() => { - originalIpc = (global as any).ipc; - (global as any).ipc = { - auth: { - loginRequest: jest.fn(), - }, - platform: { - isWindowVisible: jest.fn(), - }, - }; - - i18nService = mock({ - t: jest.fn(), - userSetLocale$: new Subject(), - locale$: new Subject(), - }); - - TestBed.configureTestingModule({ - providers: [ - DesktopLoginApprovalDialogComponentService, - { provide: I18nServiceAbstraction, useValue: i18nService }, - ], - }); - - service = TestBed.inject(DesktopLoginApprovalDialogComponentService); - }); - - afterEach(() => { - jest.clearAllMocks(); - (global as any).ipc = originalIpc; - }); - - it("is created successfully", () => { - expect(service).toBeTruthy(); - }); - - it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => { - const title = "Log in requested"; - const email = "test@bitwarden.com"; - const message = `Confirm access attempt for ${email}`; - const closeText = "Close"; - - const loginApprovalDialogComponent = { email } as LoginApprovalDialogComponent; - i18nService.t.mockImplementation((key: string) => { - switch (key) { - case "accountAccessRequested": - return title; - case "confirmAccessAttempt": - return message; - case "close": - return closeText; - default: - return ""; - } - }); - - jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false); - jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue(); - - await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email); - - expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText); - }); - - it("does not call ipc.auth.loginRequest when window is visible", async () => { - const loginApprovalDialogComponent = { - email: "test@bitwarden.com", - } as LoginApprovalDialogComponent; - - jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true); - jest.spyOn(ipc.auth, "loginRequest"); - - await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email); - - expect(ipc.auth.loginRequest).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.ts b/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.ts deleted file mode 100644 index 9c48f71990a..00000000000 --- a/apps/desktop/src/auth/login/desktop-login-approval-dialog-component.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { - DefaultLoginApprovalDialogComponentService, - LoginApprovalDialogComponentServiceAbstraction, -} from "@bitwarden/angular/auth/login-approval"; -import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; - -@Injectable() -export class DesktopLoginApprovalDialogComponentService - extends DefaultLoginApprovalDialogComponentService - implements LoginApprovalDialogComponentServiceAbstraction -{ - constructor(private i18nService: I18nServiceAbstraction) { - super(); - } - - async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise { - const isVisible = await ipc.platform.isWindowVisible(); - if (!isVisible) { - await ipc.auth.loginRequest( - this.i18nService.t("accountAccessRequested"), - this.i18nService.t("confirmAccessAttempt", email), - this.i18nService.t("close"), - ); - } - } -} diff --git a/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts new file mode 100644 index 00000000000..2caaf713473 --- /dev/null +++ b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.spec.ts @@ -0,0 +1,277 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { mockAccountInfoWith } from "@bitwarden/common/spec"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +import { DesktopAuthRequestAnsweringService } from "./desktop-auth-request-answering.service"; + +describe("DesktopAuthRequestAnsweringService", () => { + let accountService: MockProxy; + let authService: MockProxy; + let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$ + let messagingService: MockProxy; + let pendingAuthRequestsState: MockProxy; + let i18nService: MockProxy; + let logService: MockProxy; + + let sut: AuthRequestAnsweringService; + + const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId; + const userAccountInfo = mockAccountInfoWith({ + name: "User", + email: "user@example.com", + }); + const userAccount: Account = { + id: userId, + ...userAccountInfo, + }; + + const authRequestId = "auth-request-id-123"; + + beforeEach(() => { + (global as any).ipc = { + platform: { + isWindowVisible: jest.fn(), + }, + auth: { + loginRequest: jest.fn(), + }, + }; + + accountService = mock(); + authService = mock(); + masterPasswordService = { + forceSetPasswordReason$: jest.fn().mockReturnValue(of(ForceSetPasswordReason.None)), + }; + messagingService = mock(); + pendingAuthRequestsState = mock(); + i18nService = mock(); + logService = mock(); + + // Common defaults + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + accountService.activeAccount$ = of(userAccount); + accountService.accounts$ = of({ + [userId]: userAccountInfo, + }); + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + i18nService.t.mockImplementation( + (key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`, + ); + + sut = new DesktopAuthRequestAnsweringService( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + i18nService, + logService, + ); + }); + + describe("receivedPendingAuthRequest()", () => { + it("should throw if authRequestUserId not given", async () => { + // Act + const promise = sut.receivedPendingAuthRequest(undefined, undefined); + + // Assert + await expect(promise).rejects.toThrow("authRequestUserId required"); + }); + + it("should add a pending marker for the user to state", async () => { + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(pendingAuthRequestsState.add).toHaveBeenCalledTimes(1); + expect(pendingAuthRequestsState.add).toHaveBeenCalledWith(userId); + }); + + describe("given the active user is the intended recipient of the auth request, unlocked, and not required to set/change their master password", () => { + describe("given the Desktop window is visible", () => { + it("should send an 'openLoginApproval' message", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval"); + }); + + it("should NOT create a system notification", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect((global as any).ipc.auth.loginRequest).not.toHaveBeenCalled(); + }); + }); + + describe("given the Desktop window is NOT visible", () => { + it("should STILL send an 'openLoginApproval' message", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).toHaveBeenCalledTimes(1); + expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval"); + }); + + it("should create a system notification", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(false); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested"); + expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com"); + expect(i18nService.t).toHaveBeenCalledWith("close"); + + expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith( + "accountAccessRequested", + "confirmAccessAttempt:user@example.com", + "close", + ); + }); + }); + }); + + describe("given the active user is Locked", () => { + it("should NOT send an 'openLoginApproval' message", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Locked); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith( + "accountAccessRequested", + "confirmAccessAttempt:user@example.com", + "close", + ); + }); + }); + + describe("given the active user is not the intended recipient of the auth request", () => { + beforeEach(() => { + // Different active user for these tests + const differentUserId = "different-user-id" as UserId; + accountService.activeAccount$ = of({ + id: differentUserId, + ...mockAccountInfoWith({ + name: "Different User", + email: "different@example.com", + }), + }); + }); + + it("should NOT send an 'openLoginApproval' message", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + // Pass in userId, not differentUserId (the active user), to mimic an auth + // request coming in for a user who is not the active user + await sut.receivedPendingAuthRequest(userId, authRequestId); // pass in userId, not differentUserId + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + + // Act + // Pass in userId, not differentUserId (the active user), to mimic an auth + // request coming in for a user who is not the active user + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith( + "accountAccessRequested", + "confirmAccessAttempt:user@example.com", + "close", + ); + }); + }); + + describe("given the active user is required to set/change their master password", () => { + it("should NOT send an 'openLoginApproval' message", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + masterPasswordService.forceSetPasswordReason$ = jest + .fn() + .mockReturnValue(of(ForceSetPasswordReason.WeakMasterPassword)); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect(messagingService.send).not.toHaveBeenCalled(); + }); + + it("should create a system notification", async () => { + // Arrange + (global as any).ipc.platform.isWindowVisible.mockResolvedValue(true); + authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); + masterPasswordService.forceSetPasswordReason$ = jest + .fn() + .mockReturnValue(of(ForceSetPasswordReason.WeakMasterPassword)); + + // Act + await sut.receivedPendingAuthRequest(userId, authRequestId); + + // Assert + expect((global as any).ipc.auth.loginRequest).toHaveBeenCalledWith( + "accountAccessRequested", + "confirmAccessAttempt:user@example.com", + "close", + ); + }); + }); + }); +}); diff --git a/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts new file mode 100644 index 00000000000..3b2602660fe --- /dev/null +++ b/apps/desktop/src/auth/services/auth-request-answering/desktop-auth-request-answering.service.ts @@ -0,0 +1,85 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { LogService } from "@bitwarden/logging"; +import { UserId } from "@bitwarden/user-core"; + +export class DesktopAuthRequestAnsweringService + extends DefaultAuthRequestAnsweringService + implements AuthRequestAnsweringService +{ + constructor( + protected readonly accountService: AccountService, + protected readonly authService: AuthService, + protected readonly masterPasswordService: MasterPasswordServiceAbstraction, + protected readonly messagingService: MessagingService, + protected readonly pendingAuthRequestsState: PendingAuthRequestsStateService, + private readonly i18nService: I18nService, + private readonly logService: LogService, + ) { + super( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + ); + } + + /** + * @param authRequestUserId The UserId that the auth request is for. + * @param authRequestId The authRequestId param is not used on Desktop because clicks on a + * Desktop notification do not run any auth-request-specific actions. + * All clicks simply open the Desktop window. See electron-main-messaging.service.ts. + */ + async receivedPendingAuthRequest( + authRequestUserId: UserId, + authRequestId: string, + ): Promise { + if (!authRequestUserId) { + throw new Error("authRequestUserId required"); + } + + // Always persist the pending marker for this user to global state. + await this.pendingAuthRequestsState.add(authRequestUserId); + + const activeUserMeetsConditionsToShowApprovalDialog = + await this.activeUserMeetsConditionsToShowApprovalDialog(authRequestUserId); + + if (activeUserMeetsConditionsToShowApprovalDialog) { + // Send message to open dialog immediately for this request + this.messagingService.send("openLoginApproval"); + } + + const isWindowVisible = await ipc.platform.isWindowVisible(); + + // Create a system notification if either of the following are true: + // - User does NOT meet conditions to show dialog + // - User does meet conditions, but the Desktop window is not visible + // - In this second case, we both send the "openLoginApproval" message (above) AND + // also create the system notification to notify the user that the dialog is there. + if (!activeUserMeetsConditionsToShowApprovalDialog || !isWindowVisible) { + const accounts = await firstValueFrom(this.accountService.accounts$); + const accountInfo = accounts[authRequestUserId]; + + if (!accountInfo) { + this.logService.error("Account not found for authRequestUserId"); + return; + } + + const emailForUser = accountInfo.email; + await ipc.auth.loginRequest( + this.i18nService.t("accountAccessRequested"), + this.i18nService.t("confirmAccessAttempt", emailForUser), + this.i18nService.t("close"), + ); + } + } +} diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.html b/apps/desktop/src/autofill/components/autotype-shortcut.component.html index 6f73d4006ac..feb1f507c97 100644 --- a/apps/desktop/src/autofill/components/autotype-shortcut.component.html +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.html @@ -5,7 +5,7 @@

- {{ "editAutotypeShortcutDescription" | i18n }} + {{ "editAutotypeKeyboardModifiersDescription" | i18n }}

{{ "typeShortcut" | i18n }} diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts b/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts index 90aa493c596..ea394274600 100644 --- a/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.spec.ts @@ -30,11 +30,9 @@ describe("AutotypeShortcutComponent", () => { const validShortcuts = [ "Control+A", "Alt+B", - "Shift+C", "Win+D", "control+e", // case insensitive "ALT+F", - "SHIFT+G", "WIN+H", ]; @@ -46,14 +44,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should accept two modifiers with letter", () => { - const validShortcuts = [ - "Control+Alt+A", - "Control+Shift+B", - "Control+Win+C", - "Alt+Shift+D", - "Alt+Win+E", - "Shift+Win+F", - ]; + const validShortcuts = ["Control+Alt+A", "Control+Win+C", "Alt+Win+D", "Alt+Win+E"]; validShortcuts.forEach((shortcut) => { const control = createControl(shortcut); @@ -63,7 +54,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should accept modifiers in different orders", () => { - const validShortcuts = ["Alt+Control+A", "Shift+Control+B", "Win+Alt+C"]; + const validShortcuts = ["Alt+Control+A", "Win+Control+B", "Win+Alt+C"]; validShortcuts.forEach((shortcut) => { const control = createControl(shortcut); @@ -88,15 +79,14 @@ describe("AutotypeShortcutComponent", () => { const invalidShortcuts = [ "Control+1", "Alt+2", - "Shift+3", "Win+4", "Control+!", "Alt+@", - "Shift+#", + "Alt+#", "Win+$", "Control+Space", "Alt+Enter", - "Shift+Tab", + "Control+Tab", "Win+Escape", ]; @@ -111,12 +101,10 @@ describe("AutotypeShortcutComponent", () => { const invalidShortcuts = [ "Control", "Alt", - "Shift", "Win", "Control+Alt", - "Control+Shift", - "Alt+Shift", - "Control+Alt+Shift", + "Control+Win", + "Control+Alt+Win", ]; invalidShortcuts.forEach((shortcut) => { @@ -127,7 +115,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should reject shortcuts with invalid modifier names", () => { - const invalidShortcuts = ["Ctrl+A", "Command+A", "Super+A", "Meta+A", "Cmd+A", "Invalid+A"]; + const invalidShortcuts = ["Ctrl+A", "Command+A", "Meta+A", "Cmd+A", "Invalid+A"]; invalidShortcuts.forEach((shortcut) => { const control = createControl(shortcut); @@ -137,7 +125,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should reject shortcuts with multiple base keys", () => { - const invalidShortcuts = ["Control+A+B", "Alt+Ctrl+Shift"]; + const invalidShortcuts = ["Control+A+B", "Alt+Ctrl+Win"]; invalidShortcuts.forEach((shortcut) => { const control = createControl(shortcut); @@ -148,11 +136,10 @@ describe("AutotypeShortcutComponent", () => { it("should reject shortcuts with more than two modifiers", () => { const invalidShortcuts = [ - "Control+Alt+Shift+A", + "Control+Alt+Win+A", "Control+Alt+Win+B", - "Control+Shift+Win+C", - "Alt+Shift+Win+D", - "Control+Alt+Shift+Win+E", + "Control+Alt+Win+C", + "Alt+Control+Win+D", ]; invalidShortcuts.forEach((shortcut) => { @@ -221,7 +208,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should handle very long strings", () => { - const longString = "Control+Alt+Shift+Win+A".repeat(100); + const longString = "Control+Alt+Win+A".repeat(100); const control = createControl(longString); const result = validator(control); expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); @@ -230,7 +217,7 @@ describe("AutotypeShortcutComponent", () => { describe("modifier combinations", () => { it("should accept all possible single modifier combinations", () => { - const modifiers = ["Control", "Alt", "Shift", "Win"]; + const modifiers = ["Control", "Alt", "Win"]; modifiers.forEach((modifier) => { const control = createControl(`${modifier}+A`); @@ -240,14 +227,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should accept all possible two-modifier combinations", () => { - const combinations = [ - "Control+Alt+A", - "Control+Shift+A", - "Control+Win+A", - "Alt+Shift+A", - "Alt+Win+A", - "Shift+Win+A", - ]; + const combinations = ["Control+Alt+A", "Control+Win+A", "Alt+Win+A"]; combinations.forEach((shortcut) => { const control = createControl(shortcut); @@ -257,12 +237,7 @@ describe("AutotypeShortcutComponent", () => { }); it("should reject all three-modifier combinations", () => { - const combinations = [ - "Control+Alt+Shift+A", - "Control+Alt+Win+A", - "Control+Shift+Win+A", - "Alt+Shift+Win+A", - ]; + const combinations = ["Control+Alt+Win+A", "Alt+Control+Win+A", "Win+Alt+Control+A"]; combinations.forEach((shortcut) => { const control = createControl(shortcut); @@ -270,12 +245,6 @@ describe("AutotypeShortcutComponent", () => { expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); }); }); - - it("should reject all four modifiers combination", () => { - const control = createControl("Control+Alt+Shift+Win+A"); - const result = validator(control); - expect(result).toEqual({ invalidShortcut: { message: "Invalid shortcut" } }); - }); }); }); }); diff --git a/apps/desktop/src/autofill/components/autotype-shortcut.component.ts b/apps/desktop/src/autofill/components/autotype-shortcut.component.ts index 3c82d8297a1..4e1a0c2108c 100644 --- a/apps/desktop/src/autofill/components/autotype-shortcut.component.ts +++ b/apps/desktop/src/autofill/components/autotype-shortcut.component.ts @@ -77,25 +77,31 @@ export class AutotypeShortcutComponent { } } + // private buildShortcutFromEvent(event: KeyboardEvent): string | null { const hasCtrl = event.ctrlKey; const hasAlt = event.altKey; const hasShift = event.shiftKey; - const hasMeta = event.metaKey; // Windows key on Windows, Command on macOS + const hasSuper = event.metaKey; // Windows key on Windows, Command on macOS - // Require at least one modifier (Control, Alt, Shift, or Super) - if (!hasCtrl && !hasAlt && !hasShift && !hasMeta) { + // Require at least one valid modifier (Control, Alt, Super) + if (!hasCtrl && !hasAlt && !hasSuper) { return null; } const key = event.key; - // Ignore pure modifier keys themselves - if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta") { + // disallow pure modifier keys themselves + if (key === "Control" || key === "Alt" || key === "Meta") { return null; } - // Accept a single alphabetical letter as the base key + // disallow shift modifier + if (hasShift) { + return null; + } + + // require a single alphabetical letter as the base key const isAlphabetical = typeof key === "string" && /^[a-z]$/i.test(key); if (!isAlphabetical) { return null; @@ -108,10 +114,7 @@ export class AutotypeShortcutComponent { if (hasAlt) { parts.push("Alt"); } - if (hasShift) { - parts.push("Shift"); - } - if (hasMeta) { + if (hasSuper) { parts.push("Super"); } parts.push(key.toUpperCase()); @@ -129,10 +132,9 @@ export class AutotypeShortcutComponent { } // Must include exactly 1-2 modifiers and end with a single letter - // Valid examples: Ctrl+A, Shift+Z, Ctrl+Shift+X, Alt+Shift+Q + // Valid examples: Ctrl+A, Alt+B, Ctrl+Alt+X, Alt+Control+Q, Win+B, Ctrl+Win+A // Allow modifiers in any order, but only 1-2 modifiers total - const pattern = - /^(?=.*\b(Control|Alt|Shift|Win)\b)(?:Control\+|Alt\+|Shift\+|Win\+){1,2}[A-Z]$/i; + const pattern = /^(?=.*\b(Control|Alt|Win)\b)(?:Control\+|Alt\+|Win\+){1,2}[A-Z]$/i; return pattern.test(value) ? null : { invalidShortcut: { message: this.i18nService.t("invalidShortcut") } }; diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts new file mode 100644 index 00000000000..92802d2e2cf --- /dev/null +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts @@ -0,0 +1,400 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ + +import { TestBed } from "@angular/core/testing"; +import { ipcMain, globalShortcut } from "electron"; + +import { autotype } from "@bitwarden/desktop-napi"; +import { LogService } from "@bitwarden/logging"; + +import { WindowMain } from "../../main/window.main"; +import { AutotypeConfig } from "../models/autotype-config"; +import { AutotypeMatchError } from "../models/autotype-errors"; +import { AutotypeVaultData } from "../models/autotype-vault-data"; +import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels"; +import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; + +import { MainDesktopAutotypeService } from "./main-desktop-autotype.service"; + +// Mock electron modules +jest.mock("electron", () => ({ + ipcMain: { + on: jest.fn(), + removeAllListeners: jest.fn(), + }, + globalShortcut: { + register: jest.fn(), + unregister: jest.fn(), + isRegistered: jest.fn(), + }, +})); + +// Mock desktop-napi +jest.mock("@bitwarden/desktop-napi", () => ({ + autotype: { + getForegroundWindowTitle: jest.fn(), + typeInput: jest.fn(), + }, +})); + +// Mock AutotypeKeyboardShortcut +jest.mock("../models/main-autotype-keyboard-shortcut", () => ({ + AutotypeKeyboardShortcut: jest.fn().mockImplementation(() => ({ + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + })), +})); + +describe("MainDesktopAutotypeService", () => { + let service: MainDesktopAutotypeService; + let mockLogService: jest.Mocked; + let mockWindowMain: jest.Mocked; + let ipcHandlers: Map; + + beforeEach(() => { + // Track IPC handlers + ipcHandlers = new Map(); + (ipcMain.on as jest.Mock).mockImplementation((channel: string, handler: Function) => { + ipcHandlers.set(channel, handler); + }); + + // Mock LogService + mockLogService = { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warning: jest.fn(), + } as any; + + // Mock WindowMain with webContents + mockWindowMain = { + win: { + webContents: { + send: jest.fn(), + }, + }, + } as any; + + // Reset all mocks + jest.clearAllMocks(); + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(false); + (globalShortcut.register as jest.Mock).mockReturnValue(true); + + TestBed.configureTestingModule({ + providers: [ + { provide: LogService, useValue: mockLogService }, + { provide: WindowMain, useValue: mockWindowMain }, + ], + }); + + // Create service manually since it doesn't use Angular DI + service = new MainDesktopAutotypeService(mockLogService, mockWindowMain); + }); + + afterEach(() => { + ipcHandlers.clear(); // Clear handler map + service.dispose(); + }); + + describe("constructor", () => { + it("should create service", () => { + expect(service).toBeTruthy(); + }); + + it("should initialize keyboard shortcut", () => { + expect(service.autotypeKeyboardShortcut).toBeDefined(); + }); + + it("should register IPC handlers", () => { + expect(ipcMain.on).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.TOGGLE, expect.any(Function)); + expect(ipcMain.on).toHaveBeenCalledWith( + AUTOTYPE_IPC_CHANNELS.CONFIGURE, + expect.any(Function), + ); + expect(ipcMain.on).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.EXECUTE, expect.any(Function)); + expect(ipcMain.on).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + expect.any(Function), + ); + }); + }); + + describe("TOGGLE handler", () => { + it("should enable autotype when toggle is true", () => { + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype enabled."); + }); + + it("should disable autotype when toggle is false", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, false); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype disabled."); + }); + }); + + describe("CONFIGURE handler", () => { + it("should update keyboard shortcut with valid configuration", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "A"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+A"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "A"]), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockNewShortcut.set).toHaveBeenCalledWith(config.keyboardShortcut); + }); + + it("should log error with invalid keyboard shortcut", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Invalid", "Keys"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(false), + getElectronFormat: jest.fn(), + getArrayFormat: jest.fn(), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockLogService.error).toHaveBeenCalledWith( + "Configure autotype failed: the keyboard shortcut is invalid.", + ); + }); + + it("should register new shortcut if one already registered", () => { + (globalShortcut.isRegistered as jest.Mock) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "B"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(globalShortcut.register).toHaveBeenCalled(); + }); + + it("should not change shortcut if it is the same", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "B"], + }; + + jest + .spyOn(service.autotypeKeyboardShortcut, "getElectronFormat") + .mockReturnValue("Control+Alt+B"); + + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockLogService.debug).toHaveBeenCalledWith( + "setKeyboardShortcut() called but shortcut is not different from current.", + ); + }); + }); + + describe("EXECUTE handler", () => { + it("should execute autotype with valid vault data", async () => { + const vaultData: AutotypeVaultData = { + username: "testuser", + password: "testpass", + }; + + jest + .spyOn(service.autotypeKeyboardShortcut, "getArrayFormat") + .mockReturnValue(["Control", "Alt", "B"]); + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + await executeHandler({}, vaultData); + + expect(autotype.typeInput).toHaveBeenCalledWith(expect.any(Array), ["Control", "Alt", "B"]); + }); + + it("should not execute autotype with empty username", () => { + const vaultData: AutotypeVaultData = { + username: "", + password: "testpass", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + expect(autotype.typeInput).not.toHaveBeenCalled(); + }); + + it("should not execute autotype with empty password", () => { + const vaultData: AutotypeVaultData = { + username: "testuser", + password: "", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + expect(autotype.typeInput).not.toHaveBeenCalled(); + }); + + it("should format input with tab separator", () => { + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + }; + + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const vaultData: AutotypeVaultData = { + username: "user", + password: "pass", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + // Verify the input array contains char codes for "user\tpass" + const expectedPattern = "user\tpass"; + const expectedArray = Array.from(expectedPattern).map((c) => c.charCodeAt(0)); + + expect(autotype.typeInput).toHaveBeenCalledWith(expectedArray, ["Control", "Alt", "B"]); + }); + }); + + describe("completeAutotypeError handler", () => { + it("should log autotype match errors", () => { + const matchError: AutotypeMatchError = { + windowTitle: "Test Window", + errorMessage: "No matching vault item", + }; + + const errorHandler = ipcHandlers.get("autofill.completeAutotypeError"); + errorHandler({}, matchError); + + expect(mockLogService.debug).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + "No match for window: Test Window", + ); + expect(mockLogService.error).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + "No matching vault item", + ); + }); + }); + + describe("disableAutotype", () => { + it("should unregister shortcut if registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + service.disableAutotype(); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype disabled."); + }); + + it("should log debug message if shortcut not registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(false); + + service.disableAutotype(); + + expect(globalShortcut.unregister).not.toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith( + "Autotype is not registered, implicitly disabled.", + ); + }); + }); + + describe("dispose", () => { + it("should remove all IPC listeners", () => { + service.dispose(); + + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.TOGGLE); + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.EXECUTE); + }); + + it("should disable autotype", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + service.dispose(); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + }); + }); + + describe("enableAutotype (via TOGGLE handler)", () => { + it("should register global shortcut", () => { + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).toHaveBeenCalledWith("Control+Alt+B", expect.any(Function)); + }); + + it("should not register if already registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).not.toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith( + "Autotype is already enabled with this keyboard shortcut: Control+Alt+B", + ); + }); + + it("should log error if registration fails", () => { + (globalShortcut.register as jest.Mock).mockReturnValue(false); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(mockLogService.error).toHaveBeenCalledWith("Failed to enable Autotype."); + }); + + it("should send window title to renderer on shortcut activation", () => { + (autotype.getForegroundWindowTitle as jest.Mock).mockReturnValue("Notepad"); + + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + toggleHandler({}, true); + + // Get the registered callback + const registeredCallback = (globalShortcut.register as jest.Mock).mock.calls[0][1]; + registeredCallback(); + + expect(autotype.getForegroundWindowTitle).toHaveBeenCalled(); + expect(mockWindowMain.win.webContents.send).toHaveBeenCalledWith( + AUTOTYPE_IPC_CHANNELS.LISTEN, + { windowTitle: "Notepad" }, + ); + }); + }); +}); diff --git a/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts index b26be92585e..8b241ade032 100644 --- a/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts +++ b/apps/desktop/src/autofill/models/main-autotype-keyboard-shortcut.ts @@ -1,4 +1,14 @@ -import { defaultWindowsAutotypeKeyboardShortcut } from "../services/desktop-autotype.service"; +/** + Electron's representation of modifier keys + +*/ +export const CONTROL_KEY_STR = "Control"; +export const ALT_KEY_STR = "Alt"; +export const SUPER_KEY_STR = "Super"; + +export const VALID_SHORTCUT_MODIFIER_KEYS: string[] = [CONTROL_KEY_STR, ALT_KEY_STR, SUPER_KEY_STR]; + +export const DEFAULT_KEYBOARD_SHORTCUT: string[] = [CONTROL_KEY_STR, ALT_KEY_STR, "B"]; /* This class provides the following: @@ -13,7 +23,7 @@ export class AutotypeKeyboardShortcut { private autotypeKeyboardShortcut: string[]; constructor() { - this.autotypeKeyboardShortcut = defaultWindowsAutotypeKeyboardShortcut; + this.autotypeKeyboardShortcut = DEFAULT_KEYBOARD_SHORTCUT; } /* @@ -51,14 +61,16 @@ export class AutotypeKeyboardShortcut { This private function validates the strArray input to make sure the array contains valid, currently accepted shortcut keys for Windows. - Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z - Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z (not yet supported) + Valid shortcut keys: Control, Alt, Super, letters A - Z + Platform specifics: + - On Windows, Super maps to the Windows key. + - On MacOS, Super maps to the Command key. + - On MacOS, Alt maps to the Option key. See Electron keyboard shorcut docs for more info: https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts */ #keyboardShortcutIsValid(strArray: string[]) { - const VALID_SHORTCUT_CONTROL_KEYS: string[] = ["Control", "Alt", "Super", "Shift"]; const UNICODE_LOWER_BOUND = 65; // unicode 'A' const UNICODE_UPPER_BOUND = 90; // unicode 'Z' const MIN_LENGTH: number = 2; @@ -77,7 +89,7 @@ export class AutotypeKeyboardShortcut { // Ensure strArray is all modifier keys, and that the last key is a letter for (let i = 0; i < strArray.length; i++) { if (i < strArray.length - 1) { - if (!VALID_SHORTCUT_CONTROL_KEYS.includes(strArray[i])) { + if (!VALID_SHORTCUT_MODIFIER_KEYS.includes(strArray[i])) { return false; } } else { diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts index 30cc800dd28..000242476ed 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts @@ -1,6 +1,363 @@ -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; -import { getAutotypeVaultData } from "./desktop-autotype.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { DeviceType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LogService } from "@bitwarden/logging"; + +import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; +import { DesktopAutotypeService, getAutotypeVaultData } from "./desktop-autotype.service"; + +describe("DesktopAutotypeService", () => { + let service: DesktopAutotypeService; + + // Mock dependencies + let mockAccountService: jest.Mocked; + let mockAuthService: jest.Mocked; + let mockCipherService: jest.Mocked; + let mockConfigService: jest.Mocked; + let mockGlobalStateProvider: jest.Mocked; + let mockPlatformUtilsService: jest.Mocked; + let mockBillingAccountProfileStateService: jest.Mocked; + let mockDesktopAutotypePolicy: jest.Mocked; + let mockLogService: jest.Mocked; + + // Mock GlobalState objects + let mockAutotypeEnabledState: any; + let mockAutotypeKeyboardShortcutState: any; + + // BehaviorSubjects for reactive state + let autotypeEnabledSubject: BehaviorSubject; + let autotypeKeyboardShortcutSubject: BehaviorSubject; + let activeAccountSubject: BehaviorSubject; + let activeAccountStatusSubject: BehaviorSubject; + let hasPremiumSubject: BehaviorSubject; + let featureFlagSubject: BehaviorSubject; + let autotypeDefaultPolicySubject: BehaviorSubject; + let cipherViewsSubject: BehaviorSubject; + + beforeEach(() => { + // Initialize BehaviorSubjects + autotypeEnabledSubject = new BehaviorSubject(null); + autotypeKeyboardShortcutSubject = new BehaviorSubject(["Control", "Shift", "B"]); + activeAccountSubject = new BehaviorSubject({ id: "user-123" }); + activeAccountStatusSubject = new BehaviorSubject( + AuthenticationStatus.Unlocked, + ); + hasPremiumSubject = new BehaviorSubject(true); + featureFlagSubject = new BehaviorSubject(true); + autotypeDefaultPolicySubject = new BehaviorSubject(false); + cipherViewsSubject = new BehaviorSubject([]); + + // Mock GlobalState objects + mockAutotypeEnabledState = { + state$: autotypeEnabledSubject.asObservable(), + update: jest.fn().mockImplementation(async (configureState, options) => { + const newState = configureState(autotypeEnabledSubject.value, null); + + // Handle shouldUpdate option + if (options?.shouldUpdate && !options.shouldUpdate(autotypeEnabledSubject.value)) { + return autotypeEnabledSubject.value; + } + + autotypeEnabledSubject.next(newState); + return newState; + }), + }; + + mockAutotypeKeyboardShortcutState = { + state$: autotypeKeyboardShortcutSubject.asObservable(), + update: jest.fn().mockImplementation(async (configureState) => { + const newState = configureState(autotypeKeyboardShortcutSubject.value, null); + autotypeKeyboardShortcutSubject.next(newState); + return newState; + }), + }; + + // Mock GlobalStateProvider + mockGlobalStateProvider = { + get: jest.fn().mockImplementation((keyDef) => { + if (keyDef.key === "autotypeEnabled") { + return mockAutotypeEnabledState; + } + if (keyDef.key === "autotypeKeyboardShortcut") { + return mockAutotypeKeyboardShortcutState; + } + }), + } as any; + + // Mock AccountService + mockAccountService = { + activeAccount$: activeAccountSubject.asObservable(), + } as any; + + // Mock AuthService + mockAuthService = { + activeAccountStatus$: activeAccountStatusSubject.asObservable(), + } as any; + + // Mock CipherService + mockCipherService = { + cipherViews$: jest.fn().mockReturnValue(cipherViewsSubject.asObservable()), + } as any; + + // Mock ConfigService + mockConfigService = { + getFeatureFlag$: jest.fn().mockReturnValue(featureFlagSubject.asObservable()), + } as any; + + // Mock PlatformUtilsService + mockPlatformUtilsService = { + getDevice: jest.fn().mockReturnValue(DeviceType.WindowsDesktop), + } as any; + + // Mock BillingAccountProfileStateService + mockBillingAccountProfileStateService = { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(hasPremiumSubject.asObservable()), + } as any; + + // Mock DesktopAutotypeDefaultSettingPolicy + mockDesktopAutotypePolicy = { + autotypeDefaultSetting$: autotypeDefaultPolicySubject.asObservable(), + } as any; + + // Mock LogService + mockLogService = { + error: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + } as any; + + // Mock ipc (global) + global.ipc = { + autofill: { + listenAutotypeRequest: jest.fn(), + configureAutotype: jest.fn(), + toggleAutotype: jest.fn(), + }, + } as any; + + TestBed.configureTestingModule({ + providers: [ + DesktopAutotypeService, + { provide: AccountService, useValue: mockAccountService }, + { provide: AuthService, useValue: mockAuthService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: GlobalStateProvider, useValue: mockGlobalStateProvider }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { + provide: BillingAccountProfileStateService, + useValue: mockBillingAccountProfileStateService, + }, + { provide: DesktopAutotypeDefaultSettingPolicy, useValue: mockDesktopAutotypePolicy }, + { provide: LogService, useValue: mockLogService }, + ], + }); + + service = TestBed.inject(DesktopAutotypeService); + }); + + afterEach(() => { + jest.clearAllMocks(); + service.ngOnDestroy(); + }); + + describe("constructor", () => { + it("should create service", () => { + expect(service).toBeTruthy(); + }); + + it("should initialize observables", () => { + expect(service.autotypeEnabledUserSetting$).toBeDefined(); + expect(service.autotypeKeyboardShortcut$).toBeDefined(); + }); + }); + + describe("init", () => { + it("should register autotype request listener on Windows", async () => { + await service.init(); + + expect(global.ipc.autofill.listenAutotypeRequest).toHaveBeenCalled(); + }); + + it("should not initialize on non-Windows platforms", async () => { + mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop); + + await service.init(); + + expect(global.ipc.autofill.listenAutotypeRequest).not.toHaveBeenCalled(); + }); + + it("should configure autotype when keyboard shortcut changes", async () => { + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(global.ipc.autofill.configureAutotype).toHaveBeenCalled(); + }); + + it("should toggle autotype when feature enabled state changes", async () => { + autotypeEnabledSubject.next(true); + + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(global.ipc.autofill.toggleAutotype).toHaveBeenCalled(); + }); + + it("should enable autotype when policy is true and user setting is null", async () => { + autotypeEnabledSubject.next(null); + autotypeDefaultPolicySubject.next(true); + + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + }); + + describe("setAutotypeEnabledState", () => { + it("should update autotype enabled state", async () => { + await service.setAutotypeEnabledState(true); + + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + + it("should not update if value has not changed", async () => { + autotypeEnabledSubject.next(true); + + await service.setAutotypeEnabledState(true); + + // Update was called but shouldUpdate prevented the change + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + }); + + describe("setAutotypeKeyboardShortcutState", () => { + it("should update keyboard shortcut state", async () => { + const newShortcut = ["Control", "Alt", "A"]; + + await service.setAutotypeKeyboardShortcutState(newShortcut); + + expect(mockAutotypeKeyboardShortcutState.update).toHaveBeenCalled(); + expect(autotypeKeyboardShortcutSubject.value).toEqual(newShortcut); + }); + }); + + describe("matchCiphersToWindowTitle", () => { + it("should match ciphers with matching apptitle URIs", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: null, + }, + { + login: { + username: "user2", + password: "pass2", + uris: [{ uri: "apptitle://chrome" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad - Document.txt"); + + expect(result).toHaveLength(1); + expect(result[0].login.username).toBe("user1"); + }); + + it("should filter out deleted ciphers", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: new Date(), + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad"); + + expect(result).toHaveLength(0); + }); + + it("should filter out ciphers without username or password", async () => { + const mockCiphers = [ + { + login: { + username: null, + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad"); + + expect(result).toHaveLength(0); + }); + + it("should perform case-insensitive matching", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://NOTEPAD" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("notepad - document.txt"); + + expect(result).toHaveLength(1); + }); + }); + + describe("ngOnDestroy", () => { + it("should complete destroy subject", () => { + const destroySpy = jest.spyOn(service["destroy$"], "complete"); + + service.ngOnDestroy(); + + expect(destroySpy).toHaveBeenCalled(); + }); + }); +}); describe("getAutotypeVaultData", () => { it("should return vault data when cipher has username and password", () => { diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 46fec662d7a..d108577567d 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -33,11 +33,10 @@ import { UserId } from "@bitwarden/user-core"; import { AutotypeConfig } from "../models/autotype-config"; import { AutotypeVaultData } from "../models/autotype-vault-data"; +import { DEFAULT_KEYBOARD_SHORTCUT } from "../models/main-autotype-keyboard-shortcut"; import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; -export const defaultWindowsAutotypeKeyboardShortcut: string[] = ["Control", "Shift", "B"]; - export const AUTOTYPE_ENABLED = new KeyDefinition( AUTOTYPE_SETTINGS_DISK, "autotypeEnabled", @@ -72,10 +71,9 @@ export class DesktopAutotypeService implements OnDestroy { private readonly isPremiumAccount$: Observable; // The enabled/disabled state from the user settings menu - autotypeEnabledUserSetting$: Observable; + autotypeEnabledUserSetting$: Observable = of(false); - // The keyboard shortcut from the user settings menu - autotypeKeyboardShortcut$: Observable = of(defaultWindowsAutotypeKeyboardShortcut); + autotypeKeyboardShortcut$: Observable = of(DEFAULT_KEYBOARD_SHORTCUT); private destroy$ = new Subject(); @@ -106,7 +104,7 @@ export class DesktopAutotypeService implements OnDestroy { ); this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe( - map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut), + map((shortcut) => shortcut ?? DEFAULT_KEYBOARD_SHORTCUT), takeUntil(this.destroy$), ); } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 720e0dde7c2..5e2a0f41507 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nuwe URI" }, @@ -2411,6 +2479,10 @@ "message": "Is u seker u wil hierdie Send skrap?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopieer Send-skakel na knipbord", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index d6c42f5883a..7ff2f54321c 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "رابط جديد" }, @@ -2411,6 +2479,10 @@ "message": "هل أنت متأكد من أنك تريد حذف هذا الإرسال؟", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "نسخ رابط إرسال إلى الحافظة", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index ba6cad30e4f..46958d5ae3d 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Yeni", + "description": "for adding new items" + }, "newUri": { "message": "Yeni URI" }, @@ -2411,6 +2479,10 @@ "message": "Bu \"Send\"i silmək istədiyinizə əminsiniz?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "\"Send\" keçidini kopyala", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send keçidini lövhəyə kopyala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2499,7 +2571,7 @@ "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Seyfə erişmək üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış etmiş və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." }, "changePasswordWarning": { - "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saat ərzində çıxış sonlandırılacaq." + "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saat ərzində sonlandırılacaq." }, "accountRecoveryUpdateMasterPasswordSubtitle": { "message": "Hesabın geri qaytarılması prosesini tamamlamaq üçün ana parolunuzu dəyişdirin." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu giriş risk altındadır və bir veb sayt əskikdir. Daha güclü təhlükəsizlik üçün bir veb sayt əlavə edin və parolu dəyişdirin." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Əskik veb sayt" }, @@ -4031,10 +4109,16 @@ "message": "Send ilə həssas məlumatlar əmniyyətdədir", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Heç bir axtarış nəticəsi qayıtmadı" + }, "sendsBodyNoItems": { "message": "İstənilən platformada faylları və veriləri hər kəslə güvənli şəkildə paylaşın. Məlumatlarınız, ifşa olunmamaq üçün ucdan-uca şifrələnmiş qalacaq.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Filtrləri təmizləyin və ya başqa bir axtarış terminini sınayın" + }, "generatorNudgeTitle": { "message": "Cəld parol yaradın" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Yazma qısayolu" }, - "editAutotypeShortcutDescription": { - "message": "Aşağıdakı dəyişdiricilərdən birini və ya ikisini daxil edin: Ctrl, Alt, Win və ya Shift və bir hərf." + "editAutotypeKeyboardModifiersDescription": { + "message": "Aşağıdakı dəyişdiricilərdən birini və ya ikisini daxil edin: Ctrl, Alt, Win və bir hərf." }, "invalidShortcut": { "message": "Yararsız qısayol" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Arxivdən çıxart" }, + "archived": { + "message": "Arxivləndi" + }, "itemsInArchive": { "message": "Arxivdəki elementlər" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Arxivlənmiş elementlər ümumi axtarış nəticələrindən və avto-doldurma təkliflərindən xaric ediləcək. Bu elementi arxivləmək istədiyinizə əminsiniz?" }, + "unArchiveAndSave": { + "message": "Arxivdən çıxart və saxla" + }, + "restartPremium": { + "message": "\"Premium\"u yenidən başlat" + }, + "premiumSubscriptionEnded": { + "message": "Premium abunəliyiniz bitdi" + }, + "premiumSubscriptionEndedDesc": { + "message": "Arxivinizə təkrar erişmək üçün Premium abunəliyinizi yenidən başladın. Təkrar başlatmazdan əvvəl arxivlənmiş elementin detallarına düzəliş etsəniz, həmin element seyfinizə daşınacaq." + }, + "itemRestored": { + "message": "Element bərpa edildi" + }, "zipPostalCodeLabel": { "message": "ZIP / Poçt kodu" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Sessiya vaxt bitməsi" }, + "resizeSideNavigation": { + "message": "Yan naviqasiyanı yeni. ölçüləndir" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Bu ayar, təşkilatınız tərəfindən idarə olunur." }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 50f7bdc668e..eefbfc46874 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Новы URI" }, @@ -2411,6 +2479,10 @@ "message": "Вы сапраўды хочаце выдаліць гэты Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Скапіяваць спасылку на Send у буфер абмену", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 539cfb344aa..7c772655b31 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Изпращането ще бъде окончателно изтрито на тази дата.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Файл за споделяне" + }, + "hideTextByDefault": { + "message": "Скриване на текста по подразбиране" + }, + "hideYourEmail": { + "message": "Скриване на Вашата е-поща от получателите." + }, + "limitSendViews": { + "message": "Ограничаване на преглежданията" + }, + "limitSendViewsCount": { + "message": "Остават $ACCESSCOUNT$ преглеждания", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Никой няма да може да преглежда това Изпращане след достигане на ограничението.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Лична бележка" + }, + "sendDetails": { + "message": "Подробности за Изпращането", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Добавете незадължителна парола, с която получателите да имат достъп до това Изпращане.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Текст за споделяне" + }, + "newItemHeaderTextSend": { + "message": "Ново текстово Изпращане", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Ново файлово Изпращане", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Редактиране на текстовото Изпращане", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Редактиране на файловото Изпращане", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Наистина ли искате да изтриете завинаги това Изпращане?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Ново", + "description": "for adding new items" + }, "newUri": { "message": "Нов адрес" }, @@ -2411,6 +2479,10 @@ "message": "Сигурни ли сте, че искате да изтриете това изпращане?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Копиране на връзката към Изпращането", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Копиране на връзката към изпращането", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Този елемент за вписване е в риск и в него липсва уеб сайт. Добавете уеб сайт и сменете паролата, за по-добра сигурност." }, + "vulnerablePassword": { + "message": "Уязвима парола." + }, + "changeNow": { + "message": "Промяна сега" + }, "missingWebsite": { "message": "Липсващ уеб сайт" }, @@ -4031,10 +4109,16 @@ "message": "Изпращайте чувствителна информация сигурно", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Няма резултати от търсенето" + }, "sendsBodyNoItems": { "message": "Споделяйте сигурно файлове и данни с всекиго, през всяка система. Информацията Ви ще бъде защитена с шифроване от край до край, а видимостта ѝ ще бъде ограничена.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Изчистете филтрите или опитайте да търсите нещо друго" + }, "generatorNudgeTitle": { "message": "Създавайте пароли бързо" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Комбинация за въвеждане" }, - "editAutotypeShortcutDescription": { - "message": "Използвайте един или повече от модификаторите Ctrl, Alt, Win или Shift, заедно с някоя буква." + "editAutotypeKeyboardModifiersDescription": { + "message": "Използвайте един или повече от модификаторите Ctrl, Alt или Win, заедно с някоя буква." }, "invalidShortcut": { "message": "Неправилна комбинация" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Изваждане от архива" }, + "archived": { + "message": "Архивирано" + }, "itemsInArchive": { "message": "Елементи в архива" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Архивираните елементи са изключени от общите резултати при търсене и от предложенията за автоматично попълване. Наистина ли искате да архивирате този елемент?" }, + "unArchiveAndSave": { + "message": "Разархивиране и запазване" + }, + "restartPremium": { + "message": "Подновяване на платения абонамент" + }, + "premiumSubscriptionEnded": { + "message": "Вашият абонамент за платения план е приключил" + }, + "premiumSubscriptionEndedDesc": { + "message": "Ако искате отново да получите достъп до архива си, трябва да подновите платения си абонамент. Ако редактирате данните за архивиран елемент преди подновяването, той ще бъде върнат в трезора." + }, + "itemRestored": { + "message": "Записът бе възстановен" + }, "zipPostalCodeLabel": { "message": "Пощенски код" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Изтичане на времето за сесията" }, + "resizeSideNavigation": { + "message": "Преоразмеряване на страничната навигация" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Тази настройка се управлява от организацията Ви." }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index bde81f83b42..6219f374ae1 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "নতুন URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 3ed7dee9e92..622476d1836 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Novi URI" }, @@ -2411,6 +2479,10 @@ "message": "Sigurno želiš izbrisati ovaj Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiraj link Send-a", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 57041a87a31..864dce62be8 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nova URI" }, @@ -2411,6 +2479,10 @@ "message": "Esteu segur que voleu suprimir aquest Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copia l'enllaç Send al porta-retalls", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index fe0c814f2a2..cb89a325ab8 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Tento Send bude trvale smazán v určené datum.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Soubor ke sdílení" + }, + "hideTextByDefault": { + "message": "Ve výchozím nastavení skrýt text" + }, + "hideYourEmail": { + "message": "Skryje Vaši e-mailovou adresu před zobrazením." + }, + "limitSendViews": { + "message": "Omezit zobrazení" + }, + "limitSendViewsCount": { + "message": "Zbývá $ACCESSCOUNT$ zobrazení", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Po dosažení limitu nebude nikdo moci zobrazit tento Send.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Soukromá poznámka" + }, + "sendDetails": { + "message": "Podrobnosti Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Přidá volitelné heslo pro příjemce 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." + }, + "sendTypeTextToShare": { + "message": "Text ke sdílení" + }, + "newItemHeaderTextSend": { + "message": "Nový textový Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Nový Send se soubory", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Upravit textový Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Upravit Send se soubory", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Opravdu chcete tento Send trvale smazat?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Nová", + "description": "for adding new items" + }, "newUri": { "message": "Nová URI" }, @@ -2411,6 +2479,10 @@ "message": "Opravdu chcete smazat tento Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Kopírovat odkaz pro Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Zkopírovat odkaz Send do schránky", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Tyto přihlašovací údaje jsou ohrožené a chybí jim webová stránka. Přidejte webovou stránku a změňte heslo pro větší bezpečnost." }, + "vulnerablePassword": { + "message": "Zranitelné heslo." + }, + "changeNow": { + "message": "Změnit nyní" + }, "missingWebsite": { "message": "Chybějící webová stránka" }, @@ -4031,10 +4109,16 @@ "message": "Posílejte citlivé informace bezpečně", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nebyly vráceny žádné výsledky hledání" + }, "sendsBodyNoItems": { "message": "Sdílejte bezpečně soubory a data s kýmkoli na libovolné platformě. Vaše informace zůstanou šifrovány a zároveň omezují expozici.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Vymažte filtry nebo zkuste jiný hledaný výraz" + }, "generatorNudgeTitle": { "message": "Rychlé vytvoření hesla" }, @@ -4253,7 +4337,7 @@ "typeShortcut": { "message": "Napsat zkratku" }, - "editAutotypeShortcutDescription": { + "editAutotypeKeyboardModifiersDescription": { "message": "Zahrňte jeden nebo dva z následujících modifikátorů: Ctrl, Alt, Win nebo Shift a písmeno." }, "invalidShortcut": { @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Odebrat z archivu" }, + "archived": { + "message": "Archivováno" + }, "itemsInArchive": { "message": "Položky v archivu" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archivované položky jsou vyloučeny z obecných výsledků vyhledávání a z návrhů automatického vyplňování. Jste si jisti, že chcete tuto položku archivovat?" }, + "unArchiveAndSave": { + "message": "Odebrat z archivu a uložit" + }, + "restartPremium": { + "message": "Restartovat Premium" + }, + "premiumSubscriptionEnded": { + "message": "Vaše předplatné Premium skončilo" + }, + "premiumSubscriptionEndedDesc": { + "message": "Chcete-li získat přístup k Vašemu archivu, restartujte předplatné Premium. Pokud upravíte detaily archivované položky před restartováním, bude přesunuta zpět do Vašeho trezoru." + }, + "itemRestored": { + "message": "Položka byla obnovena" + }, "zipPostalCodeLabel": { "message": "ZIP / PSČ" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Časový limit relace" }, + "resizeSideNavigation": { + "message": "Změnit velikost boční navigace" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Tato nastavení je spravováno Vaší organizací." }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index af6bb971ab3..512426c218d 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 5b864c57d62..d523859005e 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Ny URI" }, @@ -2411,6 +2479,10 @@ "message": "Sikker på, at du vil slette denne Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiér Send-link til udklipsholder", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index b49654c4dbe..60480b2540f 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Das Send wird an diesem Datum dauerhaft gelöscht.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Zu teilende Datei" + }, + "hideTextByDefault": { + "message": "Text standardmäßig ausblenden" + }, + "hideYourEmail": { + "message": "Verberge deine E-Mail-Adresse vor Betrachtern." + }, + "limitSendViews": { + "message": "Ansichten begrenzen" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ Ansichten übrig", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Nach Erreichen des Limits kann niemand mehr dieses Send sehen.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private Notiz" + }, + "sendDetails": { + "message": "Send-Details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Füge ein optionales Passwort hinzu, mit dem Empfänger auf dieses Send zugreifen können.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Zu teilender Text" + }, + "newItemHeaderTextSend": { + "message": "Neues Text-Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Neues Datei-Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Text-Send bearbeiten", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Datei-Send bearbeiten", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Bist du sicher, dass du dieses Send dauerhaft löschen möchtest?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Neu", + "description": "for adding new items" + }, "newUri": { "message": "Neue URL" }, @@ -2411,6 +2479,10 @@ "message": "Bist du sicher, dass du dieses Send löschen möchtest?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Send-Link kopieren", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send-Link in Zwischenablage kopieren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, + "vulnerablePassword": { + "message": "Gefährdetes Passwort." + }, + "changeNow": { + "message": "Jetzt ändern" + }, "missingWebsite": { "message": "Fehlende Website" }, @@ -4031,10 +4109,16 @@ "message": "Sensible Informationen sicher versenden", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Keine Suchergebnisse gefunden" + }, "sendsBodyNoItems": { "message": "Teile Dateien und Daten sicher mit jedem auf jeder Plattform. Deine Informationen bleiben Ende-zu-Ende-verschlüsselt, während die Verbreitung begrenzt wird.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Filter löschen oder es mit einem anderen Suchbegriff versuchen" + }, "generatorNudgeTitle": { "message": "Passwörter schnell erstellen" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Autotype-Tastaturkürzel" }, - "editAutotypeShortcutDescription": { - "message": "Füge einen oder zwei der folgenden Modifikatoren ein: Strg, Alt, Win oder Umschalttaste, sowie einen Buchstaben." + "editAutotypeKeyboardModifiersDescription": { + "message": "Füge einen oder zwei der folgenden Modifikatoren ein: Strg, Alt, Win und einen Buchstaben." }, "invalidShortcut": { "message": "Ungültiges Tastaturkürzel" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Wiederherstellen" }, + "archived": { + "message": "Archiviert" + }, "itemsInArchive": { "message": "Einträge im Archiv" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archivierte Einträge werden von allgemeinen Suchergebnissen und Auto-Ausfüllen-Vorschlägen ausgeschlossen. Bist du sicher, dass du diesen Eintrag archivieren möchtest?" }, + "unArchiveAndSave": { + "message": "Nicht mehr archivieren und speichern" + }, + "restartPremium": { + "message": "Premium neu starten" + }, + "premiumSubscriptionEnded": { + "message": "Dein Premium-Abonnement ist abgelaufen" + }, + "premiumSubscriptionEndedDesc": { + "message": "Starte dein Premium-Abonnement neu, um den Zugriff auf dein Archiv wiederherzustellen. Wenn du die Details für einen archivierten Eintrag vor dem Neustart bearbeitest, wird er wieder zurück in deinen Tresor verschoben." + }, + "itemRestored": { + "message": "Eintrag wurde wiederhergestellt" + }, "zipPostalCodeLabel": { "message": "PLZ / Postleitzahl" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Sitzungs-Timeout" }, + "resizeSideNavigation": { + "message": "Größe der Seitennavigation ändern" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Diese Einstellung wird von deiner Organisation verwaltet." }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 68b773c74f4..ed7e21d88b8 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Νέο URI" }, @@ -2411,6 +2479,10 @@ "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το Send;", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Αντιγραφή συνδέσμου Send στο πρόχειρο", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Γρήγορη δημιουργία κωδικών πρόσβασης" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 9be96a62589..56eacc94e50 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4002,6 +4074,12 @@ }, "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" }, "missingWebsite": { "message": "Missing website" @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4461,7 +4563,7 @@ "placeholders": { "organization": { "content": "$1", - "example": "My Org Name" + "example": "My Org Name" } } }, @@ -4470,7 +4572,7 @@ "placeholders": { "organization": { "content": "$1", - "example": "My Org Name" + "example": "My Org Name" } } }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 3ad78742666..b74fb9de282 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organisation." }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 987d8d0925c..5d4d48b49c0 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "PIN" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organisation." }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 1e70ad9a180..94be4656381 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nova URI" }, @@ -2411,6 +2479,10 @@ "message": "Ĉu vi certas, ke vi volas forigi tiun Send'on?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 9ce531994a0..8426b5fe51b 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nueva URI" }, @@ -2411,6 +2479,10 @@ "message": "¿Está seguro de que quiere eliminar este envío?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copiar el enlace del Send al portapapeles", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Envía información sensible de forma segura", "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" + }, "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.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Crear contraseñas rápidamente" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Atajo inválido" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Desarchivar" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Código postal" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 0c96d531e40..6410076e603 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Uus URI" }, @@ -2411,6 +2479,10 @@ "message": "Soovid tõesti selle Sendi kustutada?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopeeri Sendi link lõikelauale", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e86f29770d6..c0665dd472d 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "URI berria" }, @@ -2411,6 +2479,10 @@ "message": "Ziur al zaude Send hau ezabatu nahi duzula?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiatu Send esteka arbelean", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 45cda22a45f..b0c4940ef0f 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "نشانی اینترنتی جدید" }, @@ -2411,6 +2479,10 @@ "message": "آیا مطمئن هستید که می‌خواهید این ارسال را حذف کنید؟", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "کپی پیوند ارسال به حافظه موقت", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "این ورود در معرض خطر است و فاقد وب‌سایت می‌باشد. برای امنیت بیشتر، یک وب‌سایت اضافه کنید و رمز عبور را تغییر دهید." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "وب‌سایت وجود ندارد" }, @@ -4031,10 +4109,16 @@ "message": "اطلاعات حساس را به‌صورت ایمن ارسال کنید", "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" + }, "sendsBodyNoItems": { "message": "پرونده‌ها و داده‌های خود را به‌صورت امن با هر کسی، در هر پلتفرمی به اشتراک بگذارید. اطلاعات شما در حین اشتراک‌گذاری به‌طور کامل رمزگذاری انتها به انتها باقی خواهد ماند و میزان افشا محدود می‌شود.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "ساخت سریع کلمات عبور" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "تایپ میانبر" }, - "editAutotypeShortcutDescription": { - "message": "شامل یک یا دو مورد از کلیدهای تغییردهنده زیر: Ctrl، Alt، Win یا Shift و یک حرف." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "میانبر نامعتبر" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "خارج کردن از بایگانی" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "آیتم‌های موجود در بایگانی" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "آیتم‌های بایگانی‌شده از نتایج جستجوی عمومی و پیشنهاد ها پر کردن خودکار حذف می‌شوند. آیا مطمئن هستید که می‌خواهید این آیتم را بایگانی کنید؟" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "کد پستی" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "وقفه زمانی نشست" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 7cee4e0c5e5..ac67919ba51 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Uusi URI" }, @@ -2411,6 +2479,10 @@ "message": "Haluatko varmasti poistaa Sendin?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopioi Send-linkki leikepöydälle", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 39651e16f4a..e62551edb98 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Bagong URI" }, @@ -2411,6 +2479,10 @@ "message": "Sigurado ka bang gusto mo na i-delete ang Ipadala na ito?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopyahin Ipadala ang link sa clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 503c93f9f19..424fb9cc124 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nouvel URI" }, @@ -2411,6 +2479,10 @@ "message": "Êtes-vous sûr de vouloir supprimer ce Send ?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copier le lien du Send dans le presse-papier", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "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." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Site Web manquant" }, @@ -4031,10 +4109,16 @@ "message": "Envoyez des informations sensibles, en toute sécurité", "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" + }, "sendsBodyNoItems": { "message": "Partagez des fichiers et des données en toute sécurité avec n'importe qui, sur n'importe quelle plateforme. Vos informations resteront chiffrées de bout en bout tout en limitant l'exposition.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Créer rapidement des mots de passe" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Saisir le raccourci" }, - "editAutotypeShortcutDescription": { - "message": "Inclure un ou deux des modificateurs suivants : Ctrl, Alt, Win, ou Shift, et une lettre." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Raccourci invalide" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Désarchiver" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Éléments dans l'archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Les éléments archivés sont exclus des résultats de recherche généraux et des suggestions de remplissage automatique. Êtes-vous sûr de vouloir archiver cet élément ?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Code postal" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Délai d'expiration de la session" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 2d0019a1f31..33a721bf8b0 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index cde90fcc7ef..876673d3aab 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "כתובת חדשה" }, @@ -2411,6 +2479,10 @@ "message": "האם אתה בטוח שברצונך למחוק סֵנְד זה?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "העתק קישור סֵנְד ללוח", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "כניסה זו נמצאת בסיכון וחסר בה אתר אינטרנט. הוסף אתר אינטרנט ושנה את הסיסמה לאבטחה חזקה יותר." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "לא נמצא אתר אינטרנט" }, @@ -4031,10 +4109,16 @@ "message": "שלח מידע רגיש באופן בטוח", "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" + }, "sendsBodyNoItems": { "message": "שתף קבצים ונתונים באופן מאובטח עם כל אחד, בכל פלטפורמה. המידע שלך יישאר מוצפן מקצה־לקצה תוך הגבלת חשיפה.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "צור סיסמאות במהירות" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "הקלד קיצור דרך" }, - "editAutotypeShortcutDescription": { - "message": "כלול אחד או שניים ממקשי הצירוף הבאים: Ctrl, Alt, Win, או Shift, ואות." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "קיצור דרך לא חוקי" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "הסר מהארכיון" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "פריטים בארכיון" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "פריטים בארכיון מוחרגים מתוצאות חיפוש כללי והצעות למילוי אוטומטי. האם אתה בטוח שברצונך להעביר פריט זה לארכיון?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "מיקוד" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "פסק זמן להפעלה" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index d3f65bef701..ddb6793f64f 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 10cf3fac635..fdb0a57dc1b 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Novi URI" }, @@ -2411,6 +2479,10 @@ "message": "Sigurno želiš izbrisati ovaj Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiraj vezu na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ova prijava je ugrožena i nedostaje joj web-stranica. Dodaj web-stranicu i promijeni lozinku za veću sigurnost." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Nedostaje web-stranica" }, @@ -4031,10 +4109,16 @@ "message": "Sigurno pošalji osjetljive podatke", "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" + }, "sendsBodyNoItems": { "message": "Sigurno dijeli datoteke i podatke s bilo kime, na bilo kojoj platformi. Tvoji podaci ostaju kriptirani uz ograničenje izloženosti.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Brzo stvori lozinke" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Vrsta prečaca" }, - "editAutotypeShortcutDescription": { - "message": "Uključi jedan ili dva modifikatora: Ctrl, Alt, Win ili Shift i slovo." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Nevažeći prečac" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Poništi arhiviranje" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Stavke u arhivi" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "Poštanski broj" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Istek sesije" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 8cb8897ab09..659360ec4a5 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "A Send véglegesen törölve lesz ebben az időpontban.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Megosztandó fájl" + }, + "hideTextByDefault": { + "message": "Szöveg elrejtése alapértelmezetten" + }, + "hideYourEmail": { + "message": "Saját email cím elrejtése a megtekintések elől." + }, + "limitSendViews": { + "message": "Megtekintések korlátozása" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ megtekintés maradt.", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Senki sem tudja megtekinteni ezt a Send elemet a korlát elérése után.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Személyes jegyzet" + }, + "sendDetails": { + "message": "Send részletek", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Adjunk meg egy opcionális jelszót a címzetteknek a Send eléréséhez.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Megosztandó szöveg" + }, + "newItemHeaderTextSend": { + "message": "Új szöveges Send elem", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Új fájl Send elem", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Send szöveg szerkesztése", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Send fájl szerkesztése", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Biztosan véglegesen törlésre kerüljön ez a Send elem?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Új", + "description": "for adding new items" + }, "newUri": { "message": "Új URI" }, @@ -2411,6 +2479,10 @@ "message": "Biztosan törlésre kerüljön ez a küldés?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Send hivatkozás másolása", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Hivatkozás küldése másolás a vágólapra", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ez a bejelentkezés veszélyben van és hiányzik egy webhely. Adjunk hozzá egy webhelyet és módosítsuk a jelszót az erősebb biztonság érdekében." }, + "vulnerablePassword": { + "message": "A jelszó sérülékeny." + }, + "changeNow": { + "message": "Módosítás most" + }, "missingWebsite": { "message": "Hiányzó webhely" }, @@ -4031,10 +4109,16 @@ "message": "Érzékeny információt küldése biztonságosan", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nincsenek visszakapott keresési eredmények." + }, "sendsBodyNoItems": { "message": "Fájlok vagy adatok megosztása biztonságosan bárkivel, bármilyen platformon. Az információk titkosítva maradnak a végpontokon, korlátozva a kitettséget.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Töröljük a szűrőket vagy próbálkozzunk másik keresési kifejezéssel." + }, "generatorNudgeTitle": { "message": "Jelszavak gyors létrehozása" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Tartalmazzon egyet vagy kettőt a következő módosítók közül: Ctrl, Alt, Win és egy betű." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Visszavétel archívumból" }, + "archived": { + "message": "Archiválva" + }, "itemsInArchive": { "message": "Archívum elemek száma" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Az archivált elemek ki vannak zárva az általános keresési eredményekből és az automatikus kitöltési javaslatokból. Biztosan archiválni szeretnénk ezt az elemet?" }, + "unArchiveAndSave": { + "message": "Archiválás visszavonása és mentés" + }, + "restartPremium": { + "message": "Prémium előfizetés újraindítása" + }, + "premiumSubscriptionEnded": { + "message": "A Prémium előfizetés véget ért." + }, + "premiumSubscriptionEndedDesc": { + "message": "Az archívumhoz hozzáférés visszaszerzéséhez indítsuk újra a Prémium előfizetést. Ha az újraindítás előtt szerkesztjük egy archivált elem adatait, akkor az visszakerül a széfbe." + }, + "itemRestored": { + "message": "Az elem visszaállításra került." + }, "zipPostalCodeLabel": { "message": "Irányítószám" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Munkamenet időkifutás" }, + "resizeSideNavigation": { + "message": "Oldalnavigáció átméretezés" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Ezt a beállítást a szervezet lezeli." }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 32a030e584c..ed02ed102bd 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "URl Baru" }, @@ -2411,6 +2479,10 @@ "message": "Anda yakin ingin menghapus Send ini?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Salin Send tautan ke papan klip", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index bc3076bbc3c..a328412149d 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Nuovo", + "description": "for adding new items" + }, "newUri": { "message": "Nuovo URI" }, @@ -2411,6 +2479,10 @@ "message": "Sei sicuro di voler eliminare questo Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copia link del Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copia il link al Send negli appunti", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Questo login è a rischio e non contiene un sito web. Aggiungi un sito web e cambia la password per maggiore sicurezza." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Sito Web mancante" }, @@ -4031,10 +4109,16 @@ "message": "Invia informazioni sensibili in modo sicuro", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nessun risultato" + }, "sendsBodyNoItems": { "message": "Condividi facilmente file e dati con chiunque, su qualsiasi piattaforma. Le tue informazioni saranno crittografate end-to-end per la massima sicurezza.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Elimina i filtri di ricerca o prova con altri termini" + }, "generatorNudgeTitle": { "message": "Crea rapidamente password sicure" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Premi i tasti da impostare per la scorciatoia" }, - "editAutotypeShortcutDescription": { - "message": "Includi uno o due dei seguenti modificatori: Ctrl, Alt, Win, o Shift, più una lettera." + "editAutotypeKeyboardModifiersDescription": { + "message": "Includi uno o due tasti modificatori a scelta tra Ctrl, Alt e Win." }, "invalidShortcut": { "message": "Scorciatoia non valida" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Rimuovi dall'Archivio" }, + "archived": { + "message": "Archiviato" + }, "itemsInArchive": { "message": "Elementi archiviati" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Gli elementi archiviati sono esclusi dai risultati di ricerca e dall'auto-riempimento. Vuoi davvero archiviare questo elemento?" }, + "unArchiveAndSave": { + "message": "Togli dall'archivio e salva" + }, + "restartPremium": { + "message": "Riavvia Premium" + }, + "premiumSubscriptionEnded": { + "message": "Il tuo abbonamento Premium è terminato" + }, + "premiumSubscriptionEndedDesc": { + "message": "Per recuperare l'accesso al tuo archivio, riavvia il tuo abbonamento Premium. Se modifichi i dettagli di un elemento archiviato prima del riavvio, sarà spostato nella tua cassaforte." + }, + "itemRestored": { + "message": "L'elemento è stato ripristinato" + }, "zipPostalCodeLabel": { "message": "CAP / codice postale" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Timeout della sessione" }, + "resizeSideNavigation": { + "message": "Ridimensiona la navigazione laterale" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Questa impostazione è gestita dalla tua organizzazione." }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index e517bdcbe72..79477e9a8ae 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "新しい URI" }, @@ -2411,6 +2479,10 @@ "message": "この Send を削除してもよろしいですか?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send リンクをクリップボードにコピー", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index caf75a81f38..8c0e9cf659c 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "ახალი URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 2d0019a1f31..33a721bf8b0 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index edfb1b6dd6e..84aa5e53262 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "ಹೊಸ ಯುಆರ್ಐ" }, @@ -2411,6 +2479,10 @@ "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಅಳಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "ನಕಲಿಸಿ ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ಲಿಂಕ್ ಕಳುಹಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 4d4e3221651..46da23b2b4a 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "새 URI" }, @@ -2411,6 +2479,10 @@ "message": "정말 이 Send를 삭제하시겠습니까?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send 링크를 클립보드에 복사", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index ead4c1d89f9..b2b18baf647 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Naujas URI" }, @@ -2411,6 +2479,10 @@ "message": "Ar tikrai norite ištrinti šį Sendą?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopijuoti Sendą į iškarpinę", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 427a6ac2e16..11348ebb5f1 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Send šajā datumā tiks neatgriezeniski izdzēsts.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Datne, ko kopīgot" + }, + "hideTextByDefault": { + "message": "Pēc noklusējuma paslēpt tekstu" + }, + "hideYourEmail": { + "message": "Paslēpt e-pasta adresi no apskatītājiem." + }, + "limitSendViews": { + "message": "Ierobežot skatījumus" + }, + "limitSendViewsCount": { + "message": "Atlikuši $ACCESSCOUNT$ skatījumi", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Neviens nevar apskatīt šo Send pēc tam, kad ir sasniegts ierobežojums.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Personiska piezīme" + }, + "sendDetails": { + "message": "Informācija par Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Pēc izvēles var pievienot paroli, lai saņēmēji varētu piekļūt šim Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Kopīgojamais teksts" + }, + "newItemHeaderTextSend": { + "message": "Jauns teksta Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Jauns datnes Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Labot teksta Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Labot datnes Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Vai tiešām neatgriezeniski izdzēst šo Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Jauns", + "description": "for adding new items" + }, "newUri": { "message": "Jauns URI" }, @@ -2411,6 +2479,10 @@ "message": "Vai tiešām izdzēst šo Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Ievietot Send saiti starpliktuvē", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Ievietot Send saiti starpliktuvē", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Šis pieteikšanās vienums ir pakļauts riskam, un tam nav norādīta tīmekļvietne. Lielākai drošībai jāpievieno tīmekļvietne un jānomaina parole." }, + "vulnerablePassword": { + "message": "Ievainojama parole." + }, + "changeNow": { + "message": "Mainīt tagad" + }, "missingWebsite": { "message": "Nav norādīta tīmekļvietne" }, @@ -4031,10 +4109,16 @@ "message": "Drošā veidā nosūti jūtīgu informāciju", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nekas netika atrasts" + }, "sendsBodyNoItems": { "message": "Kopīgo datnes un datus drošā veidā ar ikvienu jebkurā platformā! Tava informācija paliks pilnībā šifrēta, vienlaikus ierobežojot riskantumu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Jānotīra atsijātāji vai jāmēģina cits meklēšanas vaicājums" + }, "generatorNudgeTitle": { "message": "Ātra paroļu izveidošana" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Ievadīt īsinājumtaustiņus" }, - "editAutotypeShortcutDescription": { - "message": "Jāiekļauj viens vai divi no šiem taustiņiem - Ctrl, Alt, Win vai Shift - un burts." + "editAutotypeKeyboardModifiersDescription": { + "message": "Jāiekļauj viens vai divi no šiem taustiņiem: Ctrl, Alt, Win un burts." }, "invalidShortcut": { "message": "Nederīgi īsinājumtaustiņi" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Atcelt arhivēšanu" }, + "archived": { + "message": "Arhivēts" + }, "itemsInArchive": { "message": "Vienumi arhīvā" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Arhivētie vienumi netiek iekļauti vispārējās meklēšanas iznākumos un automātiskās aizpildes ieteikumos. Vai tiešām ahrivēt šo vienumu?" }, + "unArchiveAndSave": { + "message": "Atcelt arhivēšanu un saglabāt" + }, + "restartPremium": { + "message": "Atsākt Premium" + }, + "premiumSubscriptionEnded": { + "message": "Tavs Premium abonements beidzās" + }, + "premiumSubscriptionEndedDesc": { + "message": "Lai atgūtu piekļuvi savam arhīvam, jāatsāk Premium abonements. Ja labosi arhivēta vienuma informāciju pirms atsākšanas, tas tiks pārvietots atpakaļ Tavā glabātavā." + }, + "itemRestored": { + "message": "Vienums tika atjaunots" + }, "zipPostalCodeLabel": { "message": "ZIP / Pasta indekss" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Sesijas noildze" }, + "resizeSideNavigation": { + "message": "Mainīt sānu pārvietošanās joslas izmēru" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Šo iestatījumu pārvalda apvienība." }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index a5a2212b2b1..53ef1cd0c62 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Novi link" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index d71af9f752a..c13f70c64d7 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "പുതിയ URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 2d0019a1f31..33a721bf8b0 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 12c10bbcd3a..bc32f53140a 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 069250a562a..be6a2cdebb5 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Ny URI" }, @@ -2411,6 +2479,10 @@ "message": "Er du sikker på at du vil slette denne Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopier Send-lenke til utklippstavlen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index a37dc2f0b5f..846c4261187 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "नयाँ URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 824bad9508d..9b510783286 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Nieuw", + "description": "for adding new items" + }, "newUri": { "message": "Nieuwe URI" }, @@ -2411,6 +2479,10 @@ "message": "Weet je zeker dat je deze Send wilt verwijderen?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Send-koppeling kopiëren", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send-link naar klembord kopiëren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Deze login is in gevaar en mist een website. Voeg een website toe en verander het wachtwoord voor een sterkere beveiliging." }, + "vulnerablePassword": { + "message": "Kwetsbaar wachtwoord." + }, + "changeNow": { + "message": "Nu wijzigen" + }, "missingWebsite": { "message": "Ontbrekende website" }, @@ -4031,10 +4109,16 @@ "message": "Gevoelige informatie veilig versturen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Geen resultaten teruggekregen" + }, "sendsBodyNoItems": { "message": "Deel bestanden en gegevens veilig met iedereen, op elk platform. Je informatie blijft end-to-end versleuteld terwijl en blootstelling beperkt.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Wis filters of probeer een andere zoekterm" + }, "generatorNudgeTitle": { "message": "Snel wachtwoorden maken" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Typ de sneltoets" }, - "editAutotypeShortcutDescription": { - "message": "Voeg een of twee van de volgende toetsen toe: Ctrl, Alt, Win of Shift, en een letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Voeg een of twee van de volgende toetsen toe: Ctrl, Alt, Win en een letter." }, "invalidShortcut": { "message": "Ongeldige sneltoets" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Dearchiveren" }, + "archived": { + "message": "Gearchiveerd" + }, "itemsInArchive": { "message": "Items in archief" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Gearchiveerde items worden uitgesloten van algemene zoekresultaten en automatische invulsuggesties. Weet je zeker dat je dit item wilt archiveren?" }, + "unArchiveAndSave": { + "message": "Dearchiveren en opslaan" + }, + "restartPremium": { + "message": "Premium herstarten" + }, + "premiumSubscriptionEnded": { + "message": "Je Premium-abonnement is afgelopen" + }, + "premiumSubscriptionEndedDesc": { + "message": "Herstart je Premium-abonnement om toegang tot je archief te krijgen. Als je de details wijzigt voor een gearchiveerd item voor het opnieuw opstarten, zal het terug naar je kluis worden verplaatst." + }, + "itemRestored": { + "message": "Item is hersteld" + }, "zipPostalCodeLabel": { "message": "Postcode" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Sessietime-out" }, + "resizeSideNavigation": { + "message": "Formaat zijnavigatie wijzigen" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Deze instelling wordt beheerd door je organisatie." }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 3fc7075911c..8d3a9b2197a 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Ny URI" }, @@ -2411,6 +2479,10 @@ "message": "Er du sikker på at du vil fjerne denne Send-en?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopier Send-lenke til utklippstavle", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 2307e43adab..5e470bb8125 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 1724a713a30..b6d51e0627a 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "W tym dniu wysyłka zostanie trwale usunięta.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Plik wysyłki" + }, + "hideTextByDefault": { + "message": "Ukryj domyślnie tekst wysyłki" + }, + "hideYourEmail": { + "message": "Ukryj mój adres e-mail przed odbiorcami." + }, + "limitSendViews": { + "message": "Maksymalna liczba wyświetleń" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Po osiągnięciu limitu wysyłka będzie niedostępna.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Prywatna notatka" + }, + "sendDetails": { + "message": "Szczegóły wysyłki", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Zabezpiecz wysyłkę opcjonalnym hasłem.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Tekst wysyłki" + }, + "newItemHeaderTextSend": { + "message": "Nowa wysyłka tekstu", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Nowa wysyłka pliku", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edytuj tekst wysyłki", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edytuj plik wysyłki", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Czy na pewno chcesz usunąć trwale wysyłkę?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Nowy element", + "description": "for adding new items" + }, "newUri": { "message": "Nowy URI" }, @@ -2411,6 +2479,10 @@ "message": "Czy na pewno chcesz usunąć wysyłkę?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Kopiuj link wysyłki", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiuj link wysyłki do schowka", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -3923,7 +3995,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Przypomnij później" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -3944,34 +4016,34 @@ "message": "Turn on two-step login" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Zmień adres e-mail konta" }, "passkeyLogin": { - "message": "Log in with passkey?" + "message": "Zalogować się kluczem dostępu?" }, "savePasskeyQuestion": { - "message": "Save passkey?" + "message": "Zapisać klucz dostępu?" }, "saveNewPasskey": { - "message": "Save as new login" + "message": "Zapisz jako nowy dane logowania" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Zapisz klucz dostępu jako nowe dane logowania" }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "Brak pasujących danych logowania dla tej strony" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Zastąpić klucz dostępu?" }, "unableToSavePasskey": { - "message": "Unable to save passkey" + "message": "Nie można zapisać klucza dostępu" }, "alreadyContainsPasskey": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "Element zawiera już klucz dostępu. Czy na pewno chcesz zastąpić obecny klucz dostępu?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Klucz dostępu dla tej aplikacji już istnieje." }, "applicationDoesNotSupportDuplicates": { "message": "This application does not support duplicates." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Dane logowania są zagrożone i nie zawierają strony internetowej. Dodaj stronę internetową i zmień hasło." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Zmień teraz" + }, "missingWebsite": { "message": "Brak strony internetowej" }, @@ -4031,10 +4109,16 @@ "message": "Wysyłaj poufne informacje w bezpieczny sposób", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Brak pasujących elementów" + }, "sendsBodyNoItems": { "message": "Udostępniaj pliki i teksty każdemu na dowolnej platformie. Informacje będę szyfrowane end-to-end, zapewniając poufność.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Wyczyść filtry lub użyj innej frazy" + }, "generatorNudgeTitle": { "message": "Szybkie tworzenie haseł" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Rodzaj skrótu" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Skrót jest nieprawidłowy" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Usuń z archiwum" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Elementy w archiwum" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Zarchiwizowane elementy są wykluczone z wyników wyszukiwania i sugestii autouzupełniania. Czy na pewno chcesz archiwizować element?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "Kod pocztowy" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Zmień rozmiar nawigacji bocznej" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 2246233a560..31fe1915faa 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "O Send será apagado permanentemente nesta data.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Arquivo para compartilhar" + }, + "hideTextByDefault": { + "message": "Ocultar texto por padrão" + }, + "hideYourEmail": { + "message": "Ocultar seu endereço de e-mail dos visualizadores." + }, + "limitSendViews": { + "message": "Limitar visualizações" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ visualizações restantes", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Ninguém poderá visualizar este Send depois que o limite foi atingido.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Anotação privada" + }, + "sendDetails": { + "message": "Detalhes do Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Adicione uma senha opcional para que os destinatários acessem este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Texto para compartilhar" + }, + "newItemHeaderTextSend": { + "message": "Novo Send de texto", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Novo Send de arquivo", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Editar Send de texto", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Editar Send de arquivo", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Tem certeza de que quer apagar este Send permanentemente?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Criar", + "description": "for adding new items" + }, "newUri": { "message": "Novo URI" }, @@ -470,7 +538,7 @@ "message": "Use campos ocultos para dados sensíveis como senhas" }, "checkBoxHelpText": { - "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um lembrar e-mail" + "message": "Use caixas de seleção se gostaria de preencher a caixa de seleção de um formulário, como um lembrar e-mail" }, "linkedHelpText": { "message": "Use um campo vinculado quando você estiver experienciando problemas com o preenchimento automático em um site específico." @@ -2411,6 +2479,10 @@ "message": "Tem certeza que quer apagar este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copiar link do Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copiar link do Send para a área de transferência", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e está sem um site. Adicione um site e altere a senha para segurança melhor." }, + "vulnerablePassword": { + "message": "Senha vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site ausente" }, @@ -4031,10 +4109,16 @@ "message": "Envie informações sensíveis com segurança", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nenhum resultado de busca" + }, "sendsBodyNoItems": { "message": "Compartilhe dados e arquivos com segurança com qualquer pessoa, em qualquer plataforma. Suas informações permanecerão criptografadas de ponta a ponta, limitando a exposição.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Limpe os filtros ou tente outro termo de busca" + }, "generatorNudgeTitle": { "message": "Crie senhas de forma rápida" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Atalho de digitação" }, - "editAutotypeShortcutDescription": { - "message": "Inclua um ou dois dos seguintes modificadores: Ctrl, Alt, Win, ou Shift, e uma letra." + "editAutotypeKeyboardModifiersDescription": { + "message": "Inclua um ou dois dos seguintes modificadores: Ctrl, Alt, Win, e uma letra." }, "invalidShortcut": { "message": "Atalho inválido" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Desarquivar" }, + "archived": { + "message": "Arquivados" + }, "itemsInArchive": { "message": "Itens no arquivo" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Itens arquivados são excluídos dos resultados gerais de busca e das sugestões de preenchimento automático. Tem certeza de que deseja arquivar este item?" }, + "unArchiveAndSave": { + "message": "Desarquivar e salvar" + }, + "restartPremium": { + "message": "Retomar Premium" + }, + "premiumSubscriptionEnded": { + "message": "Sua assinatura Premium terminou" + }, + "premiumSubscriptionEndedDesc": { + "message": "Para recuperar o seu acesso ao seu arquivo, retoma sua assinatura Premium. Se editar detalhes de um item arquivado antes de retomar, ele será movido de volta para o seu cofre." + }, + "itemRestored": { + "message": "O item foi restaurado" + }, "zipPostalCodeLabel": { "message": "CEP / Código postal" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Limite de tempo da sessão" }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Esta configuração é gerenciada pela sua organização." }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index d0e216df217..4f35aa7bbee 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "O Send será permanentemente eliminado nesta data.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Ficheiro a partilhar" + }, + "hideTextByDefault": { + "message": "Ocultar texto por predefinição" + }, + "hideYourEmail": { + "message": "Oculte o seu endereço de e-mail dos visualizadores." + }, + "limitSendViews": { + "message": "Limitar visualizações" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ visualizações restantes", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Ninguém poderá ver este Send depois de o limite ser atingido.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Nota privada" + }, + "sendDetails": { + "message": "Detalhes do Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Adicione uma palavra-passe opcional para os destinatários acederem a este Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Texto a partilhar" + }, + "newItemHeaderTextSend": { + "message": "Novo Send de texto", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Novo Send de ficheiro", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Editar Send de texto", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Editar Send de ficheiro", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Tem a certeza de que pretende eliminar permanentemente este Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Novo", + "description": "for adding new items" + }, "newUri": { "message": "Novo URI" }, @@ -2411,6 +2479,10 @@ "message": "Tem a certeza de que pretende eliminar este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copiar link do Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copiar o link do Send para a área de transferência", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e não tem um site. Adicione um site e altere a palavra-passe para uma segurança mais forte." }, + "vulnerablePassword": { + "message": "Palavra-passe vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site em falta" }, @@ -4031,10 +4109,16 @@ "message": "Envie informações sensíveis com segurança", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Não foram apresentados resultados de pesquisa" + }, "sendsBodyNoItems": { "message": "Partilhe ficheiros e dados de forma segura com qualquer pessoa, em qualquer plataforma. As suas informações permanecerão encriptadas ponto a ponto, limitando a exposição.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Limpe os filtros ou tente outro termo de pesquisa" + }, "generatorNudgeTitle": { "message": "Criar rapidamente palavras-passe" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Introduzir atalho" }, - "editAutotypeShortcutDescription": { - "message": "Inclua um ou dois dos seguintes modificadores: Ctrl, Alt, Win, ou Shift, e uma letra." + "editAutotypeKeyboardModifiersDescription": { + "message": "Inclua um ou dois dos seguintes modificadores: Ctrl, Alt, Win e uma letra." }, "invalidShortcut": { "message": "Atalho inválido" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Desarquivar" }, + "archived": { + "message": "Arquivado" + }, "itemsInArchive": { "message": "Itens no arquivo" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Os itens arquivados são excluídos dos resultados gerais da pesquisa e das sugestões de preenchimento automático. Tem a certeza de que pretende arquivar este item?" }, + "unArchiveAndSave": { + "message": "Desarquivar e guardar" + }, + "restartPremium": { + "message": "Reiniciar Premium" + }, + "premiumSubscriptionEnded": { + "message": "A sua subscrição Premium terminou" + }, + "premiumSubscriptionEndedDesc": { + "message": "Para recuperar o acesso ao seu arquivo, reinicie a sua subscrição Premium. Se editar os detalhes de um item arquivado antes de reiniciar, ele será movido de volta para o seu cofre." + }, + "itemRestored": { + "message": "O item foi restaurado" + }, "zipPostalCodeLabel": { "message": "Código postal" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Tempo limite da sessão" }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Esta configuração é gerida pela sua organização." }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 173cf6f5f5b..05eb279529f 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "URI nou" }, @@ -2411,6 +2479,10 @@ "message": "Sigur doriți să ștergeți acest Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copiați link-ul Send-ului în clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index c0a838d86e2..5c9a0a201fe 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Новый", + "description": "for adding new items" + }, "newUri": { "message": "Новый URI" }, @@ -1776,19 +1844,19 @@ "message": "Экспорт из" }, "exportNoun": { - "message": "Export", + "message": "Экспорт", "description": "The noun form of the word Export" }, "exportVerb": { - "message": "Export", + "message": "Экспортировать", "description": "The verb form of the word Export" }, "importNoun": { - "message": "Import", + "message": "Импорт", "description": "The noun form of the word Import" }, "importVerb": { - "message": "Import", + "message": "Импортировать", "description": "The verb form of the word Import" }, "fileFormat": { @@ -2411,6 +2479,10 @@ "message": "Вы действительно хотите удалить эту Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Скопировать ссылку на Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Скопировать ссылку на Send в буфер обмена", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Этот логин подвержен риску и у него отсутствует веб-сайт. Добавьте веб-сайт и смените пароль для большей безопасности." }, + "vulnerablePassword": { + "message": "Уязвимый пароль." + }, + "changeNow": { + "message": "Изменить сейчас" + }, "missingWebsite": { "message": "Отсутствует сайт" }, @@ -4031,10 +4109,16 @@ "message": "Безопасная отправка конфиденциальной информации", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Поиск не дал результатов" + }, "sendsBodyNoItems": { "message": "Безопасно обменивайтесь файлами и данными с кем угодно на любой платформе. Ваша информация надежно шифруется и доступ к ней ограничен.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Очистите фильтры или попробуйте другой поисковый запрос" + }, "generatorNudgeTitle": { "message": "Быстрое создание паролей" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Введите сочетание клавиш" }, - "editAutotypeShortcutDescription": { - "message": "Включите один или два из следующих модификаторов: Ctrl, Alt, Win или Shift и букву." + "editAutotypeKeyboardModifiersDescription": { + "message": "Включите один или два из следующих модификаторов: Ctrl, Alt, Win и букву." }, "invalidShortcut": { "message": "Недопустимое сочетание клавиш" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Разархивировать" }, + "archived": { + "message": "Архивирован" + }, "itemsInArchive": { "message": "Элементы в архиве" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Архивированные элементы исключены из общих результатов поиска и предложений автозаполнения. Вы уверены, что хотите архивировать этот элемент?" }, + "unArchiveAndSave": { + "message": "Разархивировать и сохранить" + }, + "restartPremium": { + "message": "Переподключить Премиум" + }, + "premiumSubscriptionEnded": { + "message": "Ваша подписка Премиум закончилась" + }, + "premiumSubscriptionEndedDesc": { + "message": "Чтобы восстановить доступ к своему архиву, подключите подписку Премиум повторно. Если вы измените сведения об архивированном элементе перед переподключением, он будет перемещен обратно в ваше хранилище." + }, + "itemRestored": { + "message": "Элемент восстановлен" + }, "zipPostalCodeLabel": { "message": "Почтовый индекс" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Тайм-аут сессии" }, + "resizeSideNavigation": { + "message": "Изменить размер боковой навигации" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Эта настройка управляется вашей организацией." }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index d53bb073f49..bcbe7ae5353 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 23c3d3ae3d0..c0ec24a17d5 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Send bude natrvalo odstránený v tento deň.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Súbor, ktorý chcete zdieľať" + }, + "hideTextByDefault": { + "message": "V predvolenom nastavení skryť text" + }, + "hideYourEmail": { + "message": "Skryť moju e-mailovú adresu pri zobrazení." + }, + "limitSendViews": { + "message": "Obmedziť zobrazenia" + }, + "limitSendViewsCount": { + "message": "Zostávajúce zobrazenia: $ACCESSCOUNT$", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Po dosiahnutí limitu si tento Send nemôže nikto zobraziť.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Súkromná poznámka" + }, + "sendDetails": { + "message": "Podrobnosti o Sende", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Pridajte voliteľné heslo pre príjemcov na prístup k tomuto Sendu.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text, ktorý chcete zdieľať" + }, + "newItemHeaderTextSend": { + "message": "Nový textový Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Nový súborový Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Upraviť textový Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Upraviť súborový Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Naozaj chcete natrvalo odstrániť tento Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Nové", + "description": "for adding new items" + }, "newUri": { "message": "Nové URI" }, @@ -2411,6 +2479,10 @@ "message": "Naozaj chcete odstrániť tento Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Kopírovať odkaz na Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopírovať odkaz na Send do schránky", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Toto prihlásenie je v ohrození a chýba mu webová stránka. Pridajte webovú stránku a zmeňte heslo na silnejšie zabezpečenie." }, + "vulnerablePassword": { + "message": "Zraniteľné heslo." + }, + "changeNow": { + "message": "Zmeniť teraz" + }, "missingWebsite": { "message": "Chýbajúca webová stránka" }, @@ -4031,10 +4109,16 @@ "message": "Send, citlivé informácie bezpečne", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Nenašli sa žiadne výsledky vyhľadávania" + }, "sendsBodyNoItems": { "message": "Bezpečne zdieľajte súbory a údaje s kýmkoľvek a na akejkoľvek platforme. Vaše informácie zostanú end-to-end zašifrované a zároveň sa obmedzí ich odhalenie.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Vymažte filtre alebo zmeňte vyhľadávaný výraz" + }, "generatorNudgeTitle": { "message": "Rýchle vytváranie hesiel" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Zadajte klávesovú skratku" }, - "editAutotypeShortcutDescription": { - "message": "Použite jeden alebo dva z nasledujúcich modifikátorov: Ctrl, Alt, Win, alebo Shift a písmeno." + "editAutotypeKeyboardModifiersDescription": { + "message": "Použite jeden alebo dva z nasledujúcich modifikátorov: Ctrl, Alt, Win a písmeno." }, "invalidShortcut": { "message": "Neplatná klávesová skratka" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Zrušiť archiváciu" }, + "archived": { + "message": "Archivované" + }, "itemsInArchive": { "message": "Položky v archíve" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať túto položku?" }, + "unArchiveAndSave": { + "message": "Zrušiť archiváciu a uložiť" + }, + "restartPremium": { + "message": "Reštartovať Prémium" + }, + "premiumSubscriptionEnded": { + "message": "Vaše predplatné Prémium skončilo" + }, + "premiumSubscriptionEndedDesc": { + "message": "Ak chcete obnoviť prístup k svojmu archívu, reštartujte predplatné Prémium. Ak pred reštartom upravíte podrobnosti archivovanej položky, bude presunutá späť do trezoru." + }, + "itemRestored": { + "message": "Položka bola obnovená" + }, "zipPostalCodeLabel": { "message": "PSČ" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Časový limit relácie" }, + "resizeSideNavigation": { + "message": "Zmeniť veľkosť bočnej navigácie" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Toto nastavenie spravuje vaša organizácia." }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index b0acd957d68..54a01e0e77d 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Nov URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 0142a1bc719..b2bc5561459 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Нови УРЛ" }, @@ -2411,6 +2479,10 @@ "message": "Сигурно обрисати ово слање?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Копирај везу слања у привремену меморију", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ова пријава је ризична и недостаје веб локација. Додајте веб страницу и промените лозинку за јачу сигурност." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Недостаје веб страница" }, @@ -4031,10 +4109,16 @@ "message": "Шаљите бзбедно осетљиве информације", "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" + }, "sendsBodyNoItems": { "message": "Делите датотеке и податке безбедно са било ким, на било којој платформи. Ваше информације ће остати шифроване од почетка-до-краја уз ограничење изложености.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Брзо креирајте лозинке" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Унети пречицу" }, - "editAutotypeShortcutDescription": { - "message": "Укључите један или два следећа модификатора: Ctrl, Alt, Win, или Shift, и слово." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Неважећа пречица" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Врати из архиве" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Ставке у архиви" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Архивиране ставке су искључене из општих резултата претраге и предлога за ауто попуњавање. Јесте ли сигурни да желите да архивирате ову ставку?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP/Поштански број" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Истек сесије" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Овим подешавањем управља ваша организација." }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index e19328075d1..91330769874 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Fil att dela" + }, + "hideTextByDefault": { + "message": "Dölj text som standard" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Begränsa antalet visningar" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ visningar kvar", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Privat anteckning" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text att dela" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Ny", + "description": "for adding new items" + }, "newUri": { "message": "Ny URI" }, @@ -2411,6 +2479,10 @@ "message": "Är du säker på att du vill radera denna Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Kopiera Send-länk", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Kopiera Send-länk till urklipp", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Denna inloggning är utsatt för risk och saknar en webbplats. Lägg till en webbplats och ändra lösenordet för ökad säkerhet." }, + "vulnerablePassword": { + "message": "Sårbart lösenord." + }, + "changeNow": { + "message": "Ändra nu" + }, "missingWebsite": { "message": "Saknar webbplats" }, @@ -4031,10 +4109,16 @@ "message": "Skicka känslig information på ett säkert sätt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Inga sökresultat returnerades" + }, "sendsBodyNoItems": { "message": "Dela filer och data på ett säkert sätt med vem som helst, på vilken plattform som helst. Din information kommer att förbli krypterad från början till slut samtidigt som exponeringen begränsas.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Töm filtren eller försök med en annan sökterm" + }, "generatorNudgeTitle": { "message": "Skapa lösenord snabbt" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Inmatningsgenväg" }, - "editAutotypeShortcutDescription": { - "message": "Inkludera en eller två av följande modifierare: Ctrl, Alt, Win, eller Skift och en bokstav." + "editAutotypeKeyboardModifiersDescription": { + "message": "Inkludera en eller två av följande modifierare: Ctrl, Alt, Win och en bokstav." }, "invalidShortcut": { "message": "Ogiltig genväg" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Avarkivera" }, + "archived": { + "message": "Arkiverade" + }, "itemsInArchive": { "message": "Objekt i arkivet" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Arkiverade objekt är uteslutna från allmänna sökresultat och förslag för autofyll. Är du säker på att du vill arkivera detta objekt?" }, + "unArchiveAndSave": { + "message": "Avarkivera och spara" + }, + "restartPremium": { + "message": "Starta om Premium" + }, + "premiumSubscriptionEnded": { + "message": "Ditt Premium-abonnemang avslutades" + }, + "premiumSubscriptionEndedDesc": { + "message": "För att återfå åtkomst till ditt arkiv, starta om Premium-abonnemanget. Om du redigerar detaljer för ett arkiverat objekt innan du startar om kommer det att flyttas tillbaka till ditt valv." + }, + "itemRestored": { + "message": "Objektet har återställts" + }, "zipPostalCodeLabel": { "message": "Postnummer" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Sessionstidsgräns" }, + "resizeSideNavigation": { + "message": "Ändra storlek på sidnavigering" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Den här inställningen hanteras av din organisation." }, diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index 01f8f04f945..722110dd8ab 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "புதிய URI" }, @@ -2411,6 +2479,10 @@ "message": "இந்த Send-ஐ நீங்கள் நீக்க விரும்புகிறீர்களா?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "கிளிப்போர்டுக்கு Send இணைப்பை நகலெடுக்கவும்", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "உணர்திறன் தகவல்களைப் பாதுகாப்பாக அனுப்பவும்", "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" + }, "sendsBodyNoItems": { "message": "எந்தவொரு தளத்திலும், யாருடனும் கோப்புகளையும் தரவையும் பாதுகாப்பாகப் பகிரவும். உங்கள் தகவல் வெளிப்பாட்டைக் கட்டுப்படுத்தும்போது, அது முழுமையான குறியாக்கமாக இருக்கும்.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "கடவுச்சொற்களை விரைவாக உருவாக்கவும்" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 2d0019a1f31..33a721bf8b0 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "New URI" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index a9b08eda022..23c502ab350 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "URL ใหม่" }, @@ -2411,6 +2479,10 @@ "message": "Are you sure you want to delete this Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Copy Send link to clipboard", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -4031,10 +4109,16 @@ "message": "Send sensitive information safely", "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" + }, "sendsBodyNoItems": { "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Quickly create passwords" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Type shortcut" }, - "editAutotypeShortcutDescription": { - "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, or Shift, and a letter." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Invalid shortcut" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Items in archive" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "ZIP / Postal code" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index fb867fba82c..1626ebe8cab 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "Bu Send belirtilen tarihte kalıcı olacak silinecek.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "Paylaşılacak dosya" + }, + "hideTextByDefault": { + "message": "Metni varsayılan olarak gizle" + }, + "hideYourEmail": { + "message": "E-posta adresimi Send'i görüntüleyenlerden gizle." + }, + "limitSendViews": { + "message": "Gösterimi sınırla" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ gösterim kaldı", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "Bu sınıra ulaşıldıktan sonra bu Send'i kimse göremez.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Özel not" + }, + "sendDetails": { + "message": "Send ayrıntıları", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Alıcıların bu Send'e erişmesi için isterseniz parola ekleyebilirsiniz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Paylaşılacak metin" + }, + "newItemHeaderTextSend": { + "message": "Yeni Send metni", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "Yeni Send dosyası", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Send metnini düzenle", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Send dosyasını düzenle", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Bu Send'i kalıcı olarak silmek istediğinizden emin misiniz?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "Yeni", + "description": "for adding new items" + }, "newUri": { "message": "Yeni URI" }, @@ -2411,6 +2479,10 @@ "message": "Bu Send'i silmek istediğinizden emin misiniz?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Send bağlantısını kopyala", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Send linkini panoya kopyala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu hesap risk altında ve web sitesi eksik. Bir web sitesi ekleyin ve güvenliğinizi artırmak için parolayı değiştirin." }, + "vulnerablePassword": { + "message": "Güvensiz parola." + }, + "changeNow": { + "message": "Şimdi değiştir" + }, "missingWebsite": { "message": "Web sitesi eksik" }, @@ -4031,10 +4109,16 @@ "message": "Hassas bilgileri güvenle paylaşın", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "Hiçbir sonuç bulunamadı" + }, "sendsBodyNoItems": { "message": "Dosyaları ve verileri istediğiniz kişilerle, istediğiniz platformda paylaşın. Bilgileriniz başkalarının eline geçmemesi için uçtan şifrelenecektir.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Filtreleri temizleyin veya başka bir arama yapmayı deneyin" + }, "generatorNudgeTitle": { "message": "Hızlıca parola oluşturun" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Kısayolu yazın" }, - "editAutotypeShortcutDescription": { - "message": "Aşağıdaki değiştirici tuşlardan birini veya ikisini (Ctrl, Alt, Win ya da Shift) ve bir harf kullanın." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Geçersiz kısayol" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Arşivden çıkar" }, + "archived": { + "message": "Arşivlendi" + }, "itemsInArchive": { "message": "Arşivdeki kayıtlar" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Arşivlenmiş kayıtlar genel arama sonuçları ve otomatik doldurma önerilerinden hariç tutulur. Bu kaydı arşivlemek istediğinizden emin misiniz?" }, + "unArchiveAndSave": { + "message": "Arşivden çıkar ve kaydet" + }, + "restartPremium": { + "message": "Premium’u yeniden başlat" + }, + "premiumSubscriptionEnded": { + "message": "Premium aboneliğiniz sona erdi" + }, + "premiumSubscriptionEndedDesc": { + "message": "Arşivinize yeniden erişebilmek için Premium aboneliğinizi yeniden başlatın. Yeniden başlatmadan önce arşivlenmiş bir kaydın ayrıntılarını düzenlerseniz kayıt tekrar kasanıza taşınır." + }, + "itemRestored": { + "message": "Kayıt geri yüklendi" + }, "zipPostalCodeLabel": { "message": "ZIP / posta kodu" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Oturum zaman aşımı" }, + "resizeSideNavigation": { + "message": "Kenar menüsünü yeniden boyutlandır" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Bu ayar kuruluşunuz tarafından yönetiliyor." }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9f86087f883..65f2bb6f66f 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Новий URI" }, @@ -2411,6 +2479,10 @@ "message": "Ви дійсно хочете видалити це відправлення?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Копіювати посилання на відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Цей запис ризикований, і не має адреси вебсайту. Додайте адресу вебсайту і змініть пароль для вдосконалення безпеки." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Немає вебсайту" }, @@ -4031,10 +4109,16 @@ "message": "Безпечно надсилайте конфіденційну інформацію", "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" + }, "sendsBodyNoItems": { "message": "Безпечно діліться файлами й даними з ким завгодно, на будь-якій платформі. Ваша інформація наскрізно зашифрована та має обмежений доступ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Швидко створюйте паролі" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Введіть комбінацію клавіш" }, - "editAutotypeShortcutDescription": { - "message": "Використайте один або два таких модифікацій: Ctrl, Alt, Win, Shift, і літеру." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Недійсна комбінація клавіш" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Видобути" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Записи в архіві" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Архівовані записи виключаються з результатів звичайного пошуку та пропозицій автозаповнення. Ви дійсно хочете архівувати цей запис?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "Поштовий індекс" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Session timeout" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "This setting is managed by your organization." }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 2c248671fbb..7fc635bfae3 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "File to share" + }, + "hideTextByDefault": { + "message": "Hide text by default" + }, + "hideYourEmail": { + "message": "Hide your email address from viewers." + }, + "limitSendViews": { + "message": "Limit views" + }, + "limitSendViewsCount": { + "message": "$ACCESSCOUNT$ views left", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "No one can view this Send after the limit is reached.", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "Private note" + }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "Text to share" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "Are you sure you want to permanently delete this Send?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "New", + "description": "for adding new items" + }, "newUri": { "message": "Đường dẫn mới" }, @@ -2411,6 +2479,10 @@ "message": "Bạn có chắc chắn muốn xóa Send này?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "Copy Send link", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "Sao chép liên kết Send vào bảng nhớ tạm", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Thông tin đăng nhập này có rủi ro và thiếu một trang web. Hãy thêm trang web và đổi mật khẩu để tăng cường bảo mật." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Thiếu trang web" }, @@ -4031,10 +4109,16 @@ "message": "Gửi thông tin nhạy cảm một cách an toàn", "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" + }, "sendsBodyNoItems": { "message": "Chia sẻ tệp tin và dữ liệu một cách an toàn với bất kỳ ai, trên bất kỳ nền tảng nào. Thông tin của bạn sẽ được mã hóa đầu cuối để hạn chế rủi ro bị lộ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "Clear filters or try another search term" + }, "generatorNudgeTitle": { "message": "Tạo mật khẩu nhanh chóng" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "Phím tắt nhập liệu" }, - "editAutotypeShortcutDescription": { - "message": "Bao gồm một hoặc hai trong số các phím bổ trợ sau: Ctrl, Alt, Win hoặc Shift, và một chữ cái." + "editAutotypeKeyboardModifiersDescription": { + "message": "Include one or two of the following modifiers: Ctrl, Alt, Win, and a letter." }, "invalidShortcut": { "message": "Phím tắt không hợp lệ" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "Hủy lưu trữ" }, + "archived": { + "message": "Archived" + }, "itemsInArchive": { "message": "Các mục trong kho lưu trữ" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "Các mục đã lưu trữ sẽ bị loại khỏi kết quả tìm kiếm chung và gợi ý tự động điền. Bạn có chắc chắn muốn lưu trữ mục này không?" }, + "unArchiveAndSave": { + "message": "Unarchive and save" + }, + "restartPremium": { + "message": "Restart Premium" + }, + "premiumSubscriptionEnded": { + "message": "Your Premium subscription ended" + }, + "premiumSubscriptionEndedDesc": { + "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." + }, + "itemRestored": { + "message": "Item has been restored" + }, "zipPostalCodeLabel": { "message": "Mã ZIP / Bưu điện" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "Thời gian hết phiên" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "Cài đặt này do tổ chức của bạn quản lý." }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index b80e1cea689..3dd57eac4da 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "此 Send 将在此日期后被永久删除。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "要分享的文件" + }, + "hideTextByDefault": { + "message": "默认隐藏文本" + }, + "hideYourEmail": { + "message": "对查看者隐藏您的电子邮件地址。" + }, + "limitSendViews": { + "message": "限制查看次数" + }, + "limitSendViewsCount": { + "message": "剩余 $ACCESSCOUNT$ 次查看次数", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "达到限额后,任何人无法查看此 Send。", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "私密备注" + }, + "sendDetails": { + "message": "Send 详细信息", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "添加一个用于接收者访问此 Send 的可选密码。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "要分享的文本" + }, + "newItemHeaderTextSend": { + "message": "新增文本 Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "新增文件 Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "编辑文本 Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "编辑文件 Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "确定要永久删除此 Send 吗?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "新增", + "description": "for adding new items" + }, "newUri": { "message": "新增 URI" }, @@ -2411,6 +2479,10 @@ "message": "确定要删除此 Send 吗?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "复制 Send 链接", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "复制 Send 链接到剪贴板", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2566,7 +2638,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为 $ACTION$。", + "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为「$ACTION$」。", "placeholders": { "hours": { "content": "$1", @@ -3476,7 +3548,7 @@ "message": "清除全部" }, "plusNMore": { - "message": "另外 $QUANTITY$ 个", + "message": "还有 $QUANTITY$ 个", "placeholders": { "quantity": { "content": "$1", @@ -3785,7 +3857,7 @@ "description": "Button text to navigate back" }, "removeItem": { - "message": "删除 $NAME$", + "message": "移除 $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3974,7 +4046,7 @@ "message": "此应用程序已存在一个通行密钥。" }, "applicationDoesNotSupportDuplicates": { - "message": "此应用程序不支持重复项。" + "message": "此应用程序不支持重复。" }, "closeThisWindow": { "message": "关闭此窗口" @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登录存在风险且缺少网站。请添加网站并更改密码以增强安全性。" }, + "vulnerablePassword": { + "message": "易受攻击的密码。" + }, + "changeNow": { + "message": "立即更改" + }, "missingWebsite": { "message": "缺少网站" }, @@ -4031,10 +4109,16 @@ "message": "安全地发送敏感信息", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "未返回搜索结果" + }, "sendsBodyNoItems": { "message": "在任何平台上安全地与任何人共享文件和数据。您的信息将在限制曝光的同时保持端到端加密。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "清除筛选或尝试其他搜索词" + }, "generatorNudgeTitle": { "message": "快速创建密码" }, @@ -4253,8 +4337,8 @@ "typeShortcut": { "message": "输入快捷键" }, - "editAutotypeShortcutDescription": { - "message": "包含以下一个或两个修饰符:Ctrl、Alt、Win 或 Shift,外加一个字母。" + "editAutotypeKeyboardModifiersDescription": { + "message": "包含以下修饰键中的一个或两个:Ctrl、Alt、Win,以及一个字母键。" }, "invalidShortcut": { "message": "无效的快捷键" @@ -4292,6 +4376,9 @@ "unArchive": { "message": "取消归档" }, + "archived": { + "message": "已归档" + }, "itemsInArchive": { "message": "归档中的项目" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "已归档的项目将被排除在一般搜索结果和自动填充建议之外。确定要归档此项目吗?" }, + "unArchiveAndSave": { + "message": "取消归档并保存" + }, + "restartPremium": { + "message": "重启高级版" + }, + "premiumSubscriptionEnded": { + "message": "您的高级版订阅已结束" + }, + "premiumSubscriptionEndedDesc": { + "message": "要重新获取归档内容的访问权限,请重启您的高级版订阅。如果您在重启前编辑了某个已归档项目的详细信息,它将被移回您的密码库中。" + }, + "itemRestored": { + "message": "项目已恢复" + }, "zipPostalCodeLabel": { "message": "ZIP / 邮政编码" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "会话超时" }, + "resizeSideNavigation": { + "message": "调整侧边导航栏大小" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "此设置由您的组织管理。" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 7b5b352d5cb..43796c5b6af 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -100,6 +100,74 @@ } } }, + "deletionDateDescV2": { + "message": "此 Send 將在指定的日期後被永久刪除。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "fileToShare": { + "message": "要分享的文件" + }, + "hideTextByDefault": { + "message": "默認隱藏文字" + }, + "hideYourEmail": { + "message": "對查看者隱藏您的電子郵件地址。" + }, + "limitSendViews": { + "message": "限制查看" + }, + "limitSendViewsCount": { + "message": "剩餘 $ACCESSCOUNT$ 次查看次數", + "description": "Displayed under the limit views field on Send", + "placeholders": { + "accessCount": { + "content": "$1", + "example": "2" + } + } + }, + "limitSendViewsHint": { + "message": "在達到限額後,沒有人能查看此 Send。", + "description": "Displayed under the limit views field on Send" + }, + "privateNote": { + "message": "私人備註" + }, + "sendDetails": { + "message": "Send 詳細資訊", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendPasswordDescV3": { + "message": "新增一個用於收件人存取此 Send 的可選密碼。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendTypeTextToShare": { + "message": "要分享的文字" + }, + "newItemHeaderTextSend": { + "message": "新增文字 Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "新增檔案 Send", + "description": "Header for new file send" + }, + "editItemHeaderTextSend": { + "message": "編輯文字 Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "編輯檔案 Send", + "description": "Header for edit file send" + }, + "deleteSendPermanentConfirmation": { + "message": "您確定要永久刪除此 Send 嗎?", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "new": { + "message": "新增", + "description": "for adding new items" + }, "newUri": { "message": "新增 URI" }, @@ -2411,6 +2479,10 @@ "message": "您確定要刪除此 Send 嗎?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "copySendLink": { + "message": "複製 Send 連結", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "copySendLinkToClipboard": { "message": "複製 Send 連結到剪貼簿", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -4003,6 +4075,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登入資訊存在風險,且缺少網站。請新增網站並變更密碼以提升安全性。" }, + "vulnerablePassword": { + "message": "有安全疑慮的密碼。" + }, + "changeNow": { + "message": "立即變更" + }, "missingWebsite": { "message": "缺少網站" }, @@ -4031,10 +4109,16 @@ "message": "安全傳送機密的資訊", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsTitleNoSearchResults": { + "message": "沒有搜尋結果" + }, "sendsBodyNoItems": { "message": "安全的和任何人及任何平臺分享檔案及資料。您的資料會受到端對端加密的保護。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendsBodyNoSearchResults": { + "message": "清除過濾器或更換另一個搜尋條件" + }, "generatorNudgeTitle": { "message": "快速建立密碼" }, @@ -4253,7 +4337,7 @@ "typeShortcut": { "message": "輸入快捷鍵" }, - "editAutotypeShortcutDescription": { + "editAutotypeKeyboardModifiersDescription": { "message": "請包含以下修飾鍵之一或兩個:Ctrl、Alt、Win 或 Shift,再加上一個字母。" }, "invalidShortcut": { @@ -4292,6 +4376,9 @@ "unArchive": { "message": "取消封存" }, + "archived": { + "message": "已封存" + }, "itemsInArchive": { "message": "封存中的項目" }, @@ -4313,6 +4400,21 @@ "archiveItemConfirmDesc": { "message": "封存的項目將不會出現在一般搜尋結果或自動填入建議中。確定要封存此項目嗎?" }, + "unArchiveAndSave": { + "message": "取消封存並儲存" + }, + "restartPremium": { + "message": "重新啟用進階版" + }, + "premiumSubscriptionEnded": { + "message": "您的進階版訂閱已到期" + }, + "premiumSubscriptionEndedDesc": { + "message": "若要重新存取您的封存項目,請重新啟用進階版訂閱。若您在重新啟用前編輯封存項目的詳細資料,它將會被移回您的密碼庫。" + }, + "itemRestored": { + "message": "已還原項目" + }, "zipPostalCodeLabel": { "message": "郵編 / 郵政代碼" }, @@ -4388,6 +4490,9 @@ "sessionTimeoutHeader": { "message": "工作階段逾時" }, + "resizeSideNavigation": { + "message": "調整側邊欄大小" + }, "sessionTimeoutSettingsManagedByOrganization": { "message": "此設定由您的組織管理。" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 4734288f3c1..63b288e9161 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -50,10 +50,10 @@ import { WindowMain } from "./main/window.main"; import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main"; import { ClipboardMain } from "./platform/main/clipboard.main"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; +import { ElectronStorageService } from "./platform/main/electron-storage.service"; import { VersionMain } from "./platform/main/version.main"; import { DesktopSettingsService } from "./platform/services/desktop-settings.service"; import { ElectronLogMainService } from "./platform/services/electron-log.main.service"; -import { ElectronStorageService } from "./platform/services/electron-storage.service"; import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service"; diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index bbdd2ad0a0f..b2008d57bcd 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, ipcMain, nativeTheme, screen, session } from "electron"; +import { app, BrowserWindow, dialog, ipcMain, nativeTheme, screen, session } from "electron"; import { concatMap, firstValueFrom, pairwise } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -122,6 +122,7 @@ 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/platform/services/electron-storage.service.ts b/apps/desktop/src/platform/main/electron-storage.service.ts similarity index 93% rename from apps/desktop/src/platform/services/electron-storage.service.ts rename to apps/desktop/src/platform/main/electron-storage.service.ts index 34aa8837475..08cc5cfbd40 100644 --- a/apps/desktop/src/platform/services/electron-storage.service.ts +++ b/apps/desktop/src/platform/main/electron-storage.service.ts @@ -3,7 +3,7 @@ import * as fs from "fs"; import { ipcMain } from "electron"; -import ElectronStore from "electron-store"; +import ElectronStore, { Options as ElectronStoreOptions } from "electron-store"; import { Subject } from "rxjs"; import { @@ -35,7 +35,7 @@ export class ElectronStorageService implements AbstractStorageService { NodeUtils.mkdirpSync(dir, "700"); } const fileMode = isWindowsPortable() ? 0o666 : 0o600; - const storeConfig: ElectronStore.Options> = { + const storeConfig: ElectronStoreOptions> = { defaults: defaults, name: "data", configFileMode: fileMode, diff --git a/apps/desktop/src/platform/main/tsconfig.json b/apps/desktop/src/platform/main/tsconfig.json new file mode 100644 index 00000000000..8b49a11b574 --- /dev/null +++ b/apps/desktop/src/platform/main/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.main", + "include": ["./**/*.ts"] +} diff --git a/apps/desktop/src/scss/migration.scss b/apps/desktop/src/scss/migration.scss deleted file mode 100644 index e3078158283..00000000000 --- a/apps/desktop/src/scss/migration.scss +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Desktop UI Migration - * - * These are temporary styles during the desktop ui migration. - **/ - -/** - * This removes any padding applied by the bit-layout to content. - * This should be revisited once the table is migrated, and again once drawers are migrated. - **/ -bit-layout { - #main-content { - padding: 0 0 0 0; - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index b4082afd38c..c579e6acdc0 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -15,6 +15,5 @@ @import "left-nav.scss"; @import "loading.scss"; @import "plugins.scss"; -@import "migration.scss"; @import "../../../../libs/angular/src/scss/icons.scss"; @import "../../../../libs/components/src/multi-select/scss/bw.theme"; diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html new file mode 100644 index 00000000000..2ee78adcfb0 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.html @@ -0,0 +1,24 @@ +@if (collection().children.length) { + + @for (childCollection of collection().children; track childCollection.node.id) { + + } + +} @else { + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts new file mode 100644 index 00000000000..e23d215aef1 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/collection-filter.component.ts @@ -0,0 +1,38 @@ +import { Component, input, computed } from "@angular/core"; + +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { NavigationModule, A11yTitleDirective } from "@bitwarden/components"; +import { VaultFilter, CollectionFilter } 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: "app-collection-filter", + templateUrl: "collection-filter.component.html", + imports: [A11yTitleDirective, NavigationModule], +}) +export class CollectionFilterComponent { + protected readonly collection = input>(); + protected readonly activeFilter = input(); + + protected readonly displayName = computed(() => { + return this.collection().node.name; + }); + + protected readonly isActive = computed(() => { + return ( + this.collection().node.id === this.activeFilter()?.collectionId && + !!this.activeFilter()?.selectedCollectionNode + ); + }); + + protected applyFilter(event: Event) { + event.stopPropagation(); + + const filter = this.activeFilter(); + + if (filter) { + filter.selectedCollectionNode = this.collection(); + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html new file mode 100644 index 00000000000..f063167c48f --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.html @@ -0,0 +1,50 @@ +@if (folder().children.length) { + + @if (folder()?.node.id) { + + } + @for (childFolder of folder().children; track childFolder.node.id) { + + } + +} @else { + + @if (folder()?.node.id) { + + } + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts new file mode 100644 index 00000000000..0f24fe7aecf --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/folder-filter.component.ts @@ -0,0 +1,43 @@ +import { Component, input, computed, output } from "@angular/core"; + +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { IconButtonModule, NavigationModule, A11yTitleDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultFilter, FolderFilter } 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: "app-folder-filter", + templateUrl: "folder-filter.component.html", + imports: [A11yTitleDirective, NavigationModule, IconButtonModule, I18nPipe], +}) +export class FolderFilterComponent { + protected readonly folder = input>(); + protected readonly activeFilter = input(); + protected onEditFolder = output(); + + protected readonly displayName = computed(() => { + return this.folder().node.name; + }); + + protected readonly isActive = computed(() => { + return ( + this.folder().node.id === this.activeFilter()?.folderId && + !!this.activeFilter()?.selectedFolderNode + ); + }); + + protected applyFilter(event: Event) { + event.stopPropagation(); + const filter = this.activeFilter(); + + if (filter) { + filter.selectedFolderNode = this.folder(); + } + } + + protected editFolder(folder: FolderFilter) { + this.onEditFolder.emit(folder); + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html new file mode 100644 index 00000000000..e4e11b82a8a --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.html @@ -0,0 +1,32 @@ +@if (show()) { + + @for (organization of organizations().children ?? []; track organization.node.id) { + + @if (!organization.node.enabled) { + + + + } + } + +} 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 new file mode 100644 index 00000000000..fa91816577a --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/organization-filter.component.ts @@ -0,0 +1,81 @@ +import { Component, computed, input, inject } from "@angular/core"; + +import { DisplayMode } from "@bitwarden/angular/vault/vault-filter/models/display-mode"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { ToastService, NavigationModule, A11yTitleDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { OrganizationFilter, VaultFilter, VaultFilterServiceAbstraction } 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: "app-organization-filter", + templateUrl: "organization-filter.component.html", + imports: [A11yTitleDirective, NavigationModule, I18nPipe], +}) +export class OrganizationFilterComponent { + private toastService: ToastService = inject(ToastService); + private i18nService: I18nService = inject(I18nService); + private vaultFilterService: VaultFilterServiceAbstraction = inject(VaultFilterServiceAbstraction); + + protected readonly hide = input(false); + protected readonly organizations = input>(); + protected readonly activeFilter = input(); + protected readonly activeOrganizationDataOwnership = input(false); + protected readonly activeSingleOrganizationPolicy = input(false); + + protected readonly show = computed(() => { + const hiddenDisplayModes: DisplayMode[] = [ + "singleOrganizationAndOrganizatonDataOwnershipPolicies", + ]; + return ( + !this.hide() && + this.organizations()?.children.length > 0 && + hiddenDisplayModes.indexOf(this.displayMode()) === -1 + ); + }); + + protected readonly displayMode = computed(() => { + let displayMode: DisplayMode = "organizationMember"; + if (this.organizations() == null || this.organizations().children.length < 1) { + displayMode = "noOrganizations"; + } else if (this.activeOrganizationDataOwnership() && !this.activeSingleOrganizationPolicy()) { + displayMode = "organizationDataOwnershipPolicy"; + } else if (!this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) { + displayMode = "singleOrganizationPolicy"; + } else if (this.activeOrganizationDataOwnership() && this.activeSingleOrganizationPolicy()) { + displayMode = "singleOrganizationAndOrganizatonDataOwnershipPolicies"; + } + + return displayMode; + }); + + protected applyFilter(event: Event, organization: TreeNode) { + event.stopPropagation(); + if (!organization.node.enabled) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("disabledOrganizationFilterError"), + }); + return; + } + + this.vaultFilterService.setOrganizationFilter(organization.node); + const filter = this.activeFilter(); + + if (filter) { + filter.selectedOrganizationNode = organization; + } + } + + protected applyAllVaultsFilter() { + this.vaultFilterService.clearOrganizationFilter(); + const filter = this.activeFilter(); + + if (filter) { + filter.selectedOrganizationNode = null; + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html new file mode 100644 index 00000000000..aef9a4d41b4 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.html @@ -0,0 +1,21 @@ +@if (!hideArchive()) { + + @if (!(canArchive$ | async)) { + + } +} + diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts new file mode 100644 index 00000000000..bf51321da17 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/status-filter.component.ts @@ -0,0 +1,77 @@ +import { CommonModule } from "@angular/common"; +import { Component, viewChild, input, inject } from "@angular/core"; +import { combineLatest, firstValueFrom, map, switchMap } from "rxjs"; + +import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { NavigationModule, A11yTitleDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultFilter, CipherStatus, CipherTypeFilter } 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: "app-status-filter", + templateUrl: "status-filter.component.html", + imports: [CommonModule, A11yTitleDirective, NavigationModule, PremiumBadgeComponent, I18nPipe], +}) +export class StatusFilterComponent { + private accountService: AccountService = inject(AccountService); + private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService); + + protected readonly hideArchive = input(false); + protected readonly activeFilter = input(); + protected readonly archiveFilter: CipherTypeFilter = { + id: "archive", + name: "archiveNoun", + type: "archive", + icon: "bwi-archive", + }; + protected readonly trashFilter: CipherTypeFilter = { + id: "trash", + name: "trash", + type: "trash", + icon: "bwi-trash", + }; + + protected applyFilter(filterType: CipherStatus) { + let filter: CipherTypeFilter = null; + if (filterType === "archive") { + filter = this.archiveFilter; + } else if (filterType === "trash") { + filter = this.trashFilter; + } + + if (filter) { + this.activeFilter().selectedCipherTypeNode = new TreeNode(filter, null); + } + } + + private readonly premiumBadgeComponent = viewChild.required(PremiumBadgeComponent); + + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + protected canArchive$ = this.userId$.pipe( + switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)), + ); + + protected hasArchivedCiphers$ = this.userId$.pipe( + switchMap((userId) => + this.cipherArchiveService.archivedCiphers$(userId).pipe(map((ciphers) => ciphers.length > 0)), + ), + ); + + protected async handleArchiveFilter(event: Event) { + const [canArchive, hasArchivedCiphers] = await firstValueFrom( + combineLatest([this.canArchive$, this.hasArchivedCiphers$]), + ); + + if (canArchive || hasArchivedCiphers) { + this.applyFilter("archive"); + } else { + await this.premiumBadgeComponent().promptForPremium(event); + } + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html new file mode 100644 index 00000000000..c9807e62066 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.html @@ -0,0 +1,26 @@ + + @for (typeFilter of typeFilters$ | async; track typeFilter) { + + } + diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts new file mode 100644 index 00000000000..40755b25253 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/filters/type-filter.component.ts @@ -0,0 +1,57 @@ +import { CommonModule } from "@angular/common"; +import { Component, input, inject } from "@angular/core"; +import { map, shareReplay } from "rxjs"; + +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { NavigationModule, A11yTitleDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultFilter, CipherTypeFilter } 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: "app-type-filter", + templateUrl: "type-filter.component.html", + imports: [CommonModule, A11yTitleDirective, NavigationModule, I18nPipe], +}) +export class TypeFilterComponent { + private restrictedItemTypesService: RestrictedItemTypesService = inject( + RestrictedItemTypesService, + ); + + protected readonly cipherTypes = input>(); + protected readonly activeFilter = input(); + + protected applyTypeFilter(event: Event, cipherType: TreeNode) { + event.stopPropagation(); + const filter = this.activeFilter(); + + if (filter) { + filter.selectedCipherTypeNode = cipherType; + } + } + + protected applyAllItemsFilter(event: Event) { + const filter = this.activeFilter(); + + if (filter) { + filter.selectedCipherTypeNode = this.cipherTypes(); + } + } + + protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => + // Filter out restricted item types from the typeFilters array + this.cipherTypes().children.filter( + (type) => + !restrictedItemTypes.some( + (restrictedType) => + restrictedType.allowViewOrgIds.length === 0 && + restrictedType.cipherType === type.node.type, + ), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html new file mode 100644 index 00000000000..e0ae4687ed8 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.html @@ -0,0 +1,44 @@ +@if (!isLoaded) { +
+ +
+} @else { + + + + + @if (showCollectionsFilter()) { + + @for (collection of (collections$ | async)?.children ?? []; track collection.node.id) { + + } + + } + + @for (folder of (folders$ | async)?.children ?? []; track folder.node.id) { + + } + + +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts new file mode 100644 index 00000000000..aa54c736024 --- /dev/null +++ b/apps/desktop/src/vault/app/vault-v3/vault-filter/vault-filter.component.ts @@ -0,0 +1,143 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject, OnInit, output, computed, signal } from "@angular/core"; +import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { NavigationModule, DialogService, A11yTitleDirective } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { + OrganizationFilter, + CipherTypeFilter, + CollectionFilter, + FolderFilter, + VaultFilter, + VaultFilterServiceAbstraction as VaultFilterService, + AddEditFolderDialogComponent, + RoutedVaultFilterBridgeService, +} from "@bitwarden/vault"; + +import { DesktopPremiumUpgradePromptService } from "../../../../services/desktop-premium-upgrade-prompt.service"; + +import { CollectionFilterComponent } from "./filters/collection-filter.component"; +import { FolderFilterComponent } from "./filters/folder-filter.component"; +import { OrganizationFilterComponent } from "./filters/organization-filter.component"; +import { StatusFilterComponent } from "./filters/status-filter.component"; +import { TypeFilterComponent } from "./filters/type-filter.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-filter", + templateUrl: "vault-filter.component.html", + imports: [ + I18nPipe, + NavigationModule, + CommonModule, + OrganizationFilterComponent, + StatusFilterComponent, + TypeFilterComponent, + CollectionFilterComponent, + FolderFilterComponent, + A11yTitleDirective, + ], + providers: [ + { + provide: PremiumUpgradePromptService, + useClass: DesktopPremiumUpgradePromptService, + }, + ], +}) +export class VaultFilterComponent implements OnInit { + private routedVaultFilterBridgeService = inject(RoutedVaultFilterBridgeService); + private vaultFilterService: VaultFilterService = inject(VaultFilterService); + private accountService: AccountService = inject(AccountService); + private cipherArchiveService: CipherArchiveService = inject(CipherArchiveService); + private folderService: FolderService = inject(FolderService); + private policyService: PolicyService = inject(PolicyService); + private dialogService: DialogService = inject(DialogService); + private componentIsDestroyed$ = new Subject(); + + protected readonly activeFilter = signal(null); + protected onFilterChange = output(); + + private activeUserId: UserId; + protected isLoaded = false; + protected showArchiveVaultFilter = false; + protected activeOrganizationDataOwnershipPolicy: boolean; + protected activeSingleOrganizationPolicy: boolean; + protected organizations$: Observable>; + protected collections$: Observable>; + protected folders$: Observable>; + protected cipherTypes$: Observable>; + + protected readonly showCollectionsFilter = computed(() => { + return this.organizations$ != null && !this.activeFilter()?.isMyVaultSelected; + }); + + private async setActivePolicies() { + this.activeOrganizationDataOwnershipPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$( + PolicyType.OrganizationDataOwnership, + this.activeUserId, + ), + ); + this.activeSingleOrganizationPolicy = await firstValueFrom( + this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, this.activeUserId), + ); + } + + async ngOnInit(): Promise { + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.organizations$ = this.vaultFilterService.organizationTree$; + if ( + this.organizations$ != null && + (await firstValueFrom(this.organizations$)).children.length > 0 + ) { + await this.setActivePolicies(); + } + this.cipherTypes$ = this.vaultFilterService.cipherTypeTree$; + this.folders$ = this.vaultFilterService.folderTree$; + this.collections$ = this.vaultFilterService.collectionTree$; + + this.showArchiveVaultFilter = await firstValueFrom( + this.cipherArchiveService.hasArchiveFlagEnabled$, + ); + + this.routedVaultFilterBridgeService.activeFilter$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((filter) => { + this.activeFilter.set(filter); + }); + + this.isLoaded = true; + } + + protected async editFolder(folder: FolderFilter) { + if (!this.activeUserId) { + return; + } + const folderView = await firstValueFrom( + this.folderService.getDecrypted$(folder.id, this.activeUserId), + ); + + if (!folderView) { + return; + } + + AddEditFolderDialogComponent.open(this.dialogService, { + editFolderConfig: { + folder: { + ...folderView, + }, + }, + }); + } +} diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts index 21ba7547f8b..64f850826a3 100644 --- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts +++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts @@ -1,40 +1,38 @@ import { CommonModule } from "@angular/common"; -import { - ChangeDetectorRef, - Component, - NgZone, - OnDestroy, - OnInit, - ViewChild, - ViewContainerRef, -} from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs"; +import { + firstValueFrom, + Subject, + takeUntil, + switchMap, + lastValueFrom, + Observable, + from, +} from "rxjs"; import { filter, map, take } from "rxjs/operators"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +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 { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { 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 { 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 { getByIds } from "@bitwarden/common/platform/misc"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +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"; @@ -60,8 +58,6 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { - AddEditFolderDialogComponent, - AddEditFolderDialogResult, AttachmentDialogResult, AttachmentsV2Component, ChangeLoginPasswordService, @@ -78,6 +74,9 @@ import { PasswordRepromptService, CipherFormComponent, ArchiveCipherUtilitiesService, + VaultFilter, + VaultFilterServiceAbstraction as VaultFilterService, + RoutedVaultFilterBridgeService, } from "@bitwarden/vault"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; @@ -86,8 +85,6 @@ import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-pr import { invokeMenu, RendererMenuItem } from "../../../utils"; import { AssignCollectionsDesktopComponent } from "../vault/assign-collections"; import { ItemFooterComponent } from "../vault/item-footer.component"; -import { VaultFilterComponent } from "../vault/vault-filter/vault-filter.component"; -import { VaultFilterModule } from "../vault/vault-filter/vault-filter.module"; import { VaultItemsV2Component } from "../vault/vault-items-v2.component"; const BroadcasterSubscriptionId = "VaultComponent"; @@ -107,7 +104,6 @@ const BroadcasterSubscriptionId = "VaultComponent"; ItemModule, ButtonModule, PremiumBadgeComponent, - VaultFilterModule, VaultItemsV2Component, ], providers: [ @@ -134,21 +130,11 @@ const BroadcasterSubscriptionId = "VaultComponent"; }, ], }) -export class VaultComponent - implements OnInit, OnDestroy, CopyClickListener -{ +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(VaultFilterComponent, { static: true }) - vaultFilterComponent: VaultFilterComponent | null = null; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) - folderAddEditModalRef: ViewContainerRef | null = null; + 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) @@ -158,7 +144,7 @@ export class VaultComponent cipherId: string | null = null; favorites = false; type: CipherType | null = null; - folderId: string | null = null; + folderId: string | null | undefined = null; collectionId: string | null = null; organizationId: string | null = null; myVaultOnly = false; @@ -194,6 +180,7 @@ export class VaultComponent private componentIsDestroyed$ = new Subject(); private allOrganizations: Organization[] = []; private allCollections: CollectionView[] = []; + private filteredCollections: CollectionView[] = []; constructor( private route: ActivatedRoute, @@ -209,7 +196,6 @@ export class VaultComponent private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, private searchBarService: SearchBarService, - private apiService: ApiService, private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, @@ -220,11 +206,12 @@ export class VaultComponent private collectionService: CollectionService, private organizationService: OrganizationService, private folderService: FolderService, - private configService: ConfigService, private authRequestService: AuthRequestServiceAbstraction, private cipherArchiveService: CipherArchiveService, private policyService: PolicyService, private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService, + private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, + private vaultFilterService: VaultFilterService, ) {} async ngOnInit() { @@ -240,6 +227,14 @@ export class VaultComponent this.userHasPremiumAccess = canAccessPremium; }); + // Subscribe to filter changes from router params via the bridge service + this.routedVaultFilterBridgeService.activeFilter$ + .pipe( + switchMap((vaultFilter: VaultFilter) => from(this.applyVaultFilter(vaultFilter))), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe(); + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone .run(async () => { @@ -267,15 +262,7 @@ export class VaultComponent break; case "syncCompleted": if (this.vaultItemsComponent) { - await this.vaultItemsComponent - .reload(this.activeFilter.buildFilter()) - .catch(() => {}); - } - if (this.vaultFilterComponent) { - await this.vaultFilterComponent - .reloadCollectionsAndFolders(this.activeFilter) - .catch(() => {}); - await this.vaultFilterComponent.reloadOrganizations().catch(() => {}); + await this.vaultItemsComponent.refresh().catch(() => {}); } break; case "modalShown": @@ -377,6 +364,12 @@ export class VaultComponent .subscribe((collections) => { this.allCollections = collections; }); + + this.vaultFilterService.filteredCollections$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((collections) => { + this.filteredCollections = collections; + }); } ngOnDestroy() { @@ -403,19 +396,6 @@ export class VaultComponent this.addType = paramCipherAddType; await this.addCipher(this.addType).catch(() => {}); } - - const paramCipherType = toCipherType(params.type); - this.activeFilter = new VaultFilter({ - status: params.deleted ? "trash" : params.favorites ? "favorites" : "all", - cipherType: params.action === "add" || paramCipherType == null ? undefined : paramCipherType, - selectedFolderId: params.folderId, - selectedCollectionId: params.selectedCollectionId, - selectedOrganizationId: params.selectedOrganizationId, - myVaultOnly: params.myVaultOnly ?? false, - }); - if (this.vaultItemsComponent) { - await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); - } } /** @@ -439,9 +419,7 @@ export class VaultComponent this.cipherId = cipher.id; this.cipher = cipher; this.collections = - this.vaultFilterComponent?.collections?.fullList.filter((c) => - cipher.collectionIds.includes(c.id), - ) ?? null; + this.filteredCollections?.filter((c) => cipher.collectionIds.includes(c.id)) ?? null; this.action = "view"; await this.go().catch(() => {}); @@ -798,19 +776,45 @@ export class VaultComponent await this.go().catch(() => {}); } + /** + * Wraps a filter function to handle CipherListView objects. + * CipherListView has a different type structure where type can be a string or object. + * This wrapper converts it to CipherView-compatible structure before filtering. + */ + private wrapFilterForCipherListView( + filterFn: (cipher: CipherView) => boolean, + ): (cipher: CipherViewLike) => boolean { + return (cipher: CipherViewLike) => { + // For CipherListView, create a proxy object with the correct type property + if (CipherViewLikeUtils.isCipherListView(cipher)) { + const proxyCipher = { + ...cipher, + type: CipherViewLikeUtils.getType(cipher), + // Normalize undefined organizationId to null for filter compatibility + organizationId: cipher.organizationId ?? null, + // Explicitly include isDeleted and isArchived since they might be getters + isDeleted: CipherViewLikeUtils.isDeleted(cipher), + isArchived: CipherViewLikeUtils.isArchived(cipher), + }; + return filterFn(proxyCipher as any); + } + }; + } + async applyVaultFilter(vaultFilter: VaultFilter) { this.searchBarService.setPlaceholderText( this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), ); this.activeFilter = vaultFilter; - await this.vaultItemsComponent - ?.reload( - this.activeFilter.buildFilter(), - vaultFilter.status === "trash", - vaultFilter.status === "archive", - ) - .catch(() => {}); - await this.go().catch(() => {}); + + const originalFilterFn = this.activeFilter.buildFilter(); + const wrappedFilterFn = this.wrapFilterForCipherListView(originalFilterFn); + + await this.vaultItemsComponent?.reload( + wrappedFilterFn, + vaultFilter.isDeleted, + vaultFilter.isArchived, + ); } private getAvailableCollections(cipher: CipherView): CollectionView[] { @@ -824,25 +828,25 @@ export class VaultComponent } private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { - if (vaultFilter.status === "favorites") { + if (vaultFilter.isFavorites) { return "searchFavorites"; } - if (vaultFilter.status === "trash") { + if (vaultFilter.isDeleted) { return "searchTrash"; } if (vaultFilter.cipherType != null) { return "searchType"; } - if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId !== "none") { + if (vaultFilter.folderId != null && vaultFilter.folderId !== "none") { return "searchFolder"; } - if (vaultFilter.selectedCollectionId != null) { + if (vaultFilter.collectionId != null) { return "searchCollection"; } - if (vaultFilter.selectedOrganizationId != null) { + if (vaultFilter.organizationId != null) { return "searchOrganization"; } - if (vaultFilter.myVaultOnly) { + if (vaultFilter.isMyVaultSelected) { return "searchMyVault"; } return "searchVault"; @@ -863,23 +867,6 @@ export class VaultComponent if (!folderView) { return; } - - const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, { - editFolderConfig: { - folder: { - ...folderView, - }, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if ( - result === AddEditFolderDialogResult.Deleted || - result === AddEditFolderDialogResult.Created - ) { - await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter); - } } /** Refresh the current cipher object */ @@ -919,19 +906,13 @@ export class VaultComponent queryParams = { action: this.action, cipherId: this.cipherId, - favorites: this.favorites ? true : null, - type: this.type, - folderId: this.folderId, - collectionId: this.collectionId, - deleted: this.deleted ? true : null, - organizationId: this.organizationId, - myVaultOnly: this.myVaultOnly, }; } this.router .navigate([], { relativeTo: this.route, queryParams: queryParams, + queryParamsHandling: "merge", replaceUrl: true, }) .catch(() => {}); @@ -966,22 +947,22 @@ export class VaultComponent } private prefillCipherFromFilter() { - if (this.activeFilter.selectedCollectionId != null && this.vaultFilterComponent != null) { - const collections = this.vaultFilterComponent.collections?.fullList.filter( - (c) => c.id === this.activeFilter.selectedCollectionId, + if (this.activeFilter.collectionId != null) { + const collections = this.filteredCollections?.filter( + (c) => c.id === this.activeFilter.collectionId, ); - if (collections.length > 0) { + if (collections?.length > 0) { this.addOrganizationId = collections[0].organizationId; - this.addCollectionIds = [this.activeFilter.selectedCollectionId]; + this.addCollectionIds = [this.activeFilter.collectionId]; } - } else if (this.activeFilter.selectedOrganizationId) { - this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } else if (this.activeFilter.organizationId) { + this.addOrganizationId = this.activeFilter.organizationId; } else { // clear out organizationId when the user switches to a personal vault filter this.addOrganizationId = null; } - if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { - this.folderId = this.activeFilter.selectedFolderId; + if (this.activeFilter.folderId && this.activeFilter.selectedFolderNode) { + this.folderId = this.activeFilter.folderId; } if (this.config == null) { @@ -990,7 +971,9 @@ export class VaultComponent this.config.initialValues = { ...this.config.initialValues, + folderId: this.folderId, organizationId: this.addOrganizationId as OrganizationId, + collectionIds: this.addCollectionIds as CollectionId[], }; } diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html index 859b2f1bdc5..a03f3e96b06 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.html +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -7,9 +7,9 @@ [hidden]="action === 'view'" bitButton class="primary" - appA11yTitle="{{ 'save' | i18n }}" + appA11yTitle="{{ submitButtonText() }}" > - {{ "save" | i18n }} + {{ submitButtonText() }} 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 0ac12c928f2..c80e4e59ae4 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.ts +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -8,6 +8,7 @@ import { ViewChild, OnChanges, SimpleChanges, + input, } from "@angular/core"; import { combineLatest, firstValueFrom, switchMap } from "rxjs"; @@ -67,6 +68,8 @@ export class ItemFooterComponent implements OnInit, OnChanges { // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null; + readonly submitButtonText = input(this.i18nService.t("save")); + activeUserId: UserId | null = null; passwordReprompted: boolean = false; @@ -218,7 +221,7 @@ export class ItemFooterComponent implements OnInit, OnChanges { } private async checkArchiveState() { - const cipherCanBeArchived = !this.cipher.isDeleted && this.cipher.organizationId == null; + const cipherCanBeArchived = !this.cipher.isDeleted; const [userCanArchive, hasArchiveFlagEnabled] = await firstValueFrom( this.accountService.activeAccount$.pipe( getUserId, diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html index fc14700a7af..84c0cd8a1fb 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -2,6 +2,21 @@
+ @if (showPremiumCallout()) { +
+ + +
+ {{ "premiumSubscriptionEndedDesc" | i18n }} +
+ + {{ "restartPremium" | i18n }} + +
+
+
+ } +
extends BaseVaultItemsComponent { + readonly showPremiumCallout = input(false); + readonly organizationId = input(undefined); + protected CipherViewLikeUtils = CipherViewLikeUtils; + constructor( searchService: SearchService, private readonly searchBarService: SearchBarService, @@ -37,6 +43,7 @@ export class VaultItemsV2Component extends BaseVaultIt accountService: AccountService, restrictedItemTypesService: RestrictedItemTypesService, configService: ConfigService, + private premiumUpgradePromptService: PremiumUpgradePromptService, ) { super(searchService, cipherService, accountService, restrictedItemTypesService, configService); @@ -47,6 +54,10 @@ export class VaultItemsV2Component extends BaseVaultIt }); } + async navigateToGetPremium() { + await this.premiumUpgradePromptService.promptForPremium(this.organizationId()); + } + trackByFn(index: number, c: C): string { return uuidAsString(c.id!); } diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html index 2696dd0d452..d10b3fd85c6 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -6,13 +6,15 @@ (onCipherClicked)="viewCipher($event)" (onCipherRightClicked)="viewCipherMenu($event)" (onAddCipher)="addCipher($event)" + [showPremiumCallout]="showPremiumCallout$ | async" + [organizationId]="organizationId" >
- + cipherId: string | null = null; favorites = false; type: CipherType | null = null; - folderId: string | null = null; + folderId: string | null | undefined = null; collectionId: string | null = null; - organizationId: string | null = null; + organizationId: OrganizationId | null = null; myVaultOnly = false; addType: CipherType | undefined = undefined; addOrganizationId: string | null = null; @@ -172,11 +184,25 @@ export class VaultV2Component deleted = false; userHasPremiumAccess = false; activeFilter: VaultFilter = new VaultFilter(); + private activeFilterSubject = new BehaviorSubject(new VaultFilter()); + private activeFilter$ = this.activeFilterSubject.asObservable(); + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + showPremiumCallout$ = this.userId$.pipe( + switchMap((userId) => + combineLatest([ + this.activeFilter$, + this.cipherArchiveService.showSubscriptionEndedMessaging$(userId), + ]).pipe( + map(([activeFilter, showMessaging]) => activeFilter.status === "archive" && showMessaging), + ), + ), + ); activeUserId: UserId | null = null; cipherRepromptId: string | null = null; - cipher: CipherView | null = new CipherView(); + readonly cipher = signal(null); collections: CollectionView[] | null = null; config: CipherFormConfig | null = null; + readonly userHasPremium = signal(false); /** Tracks the disabled status of the edit cipher form */ protected formDisabled: boolean = false; @@ -187,12 +213,13 @@ export class VaultV2Component switchMap((id) => this.organizationService.organizations$(id)), ); - protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( - filter((account): account is Account => !!account), - switchMap((account) => - this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - ), - ); + protected readonly submitButtonText = computed(() => { + return this.cipher()?.isArchived && + !this.userHasPremium() && + this.cipherArchiveService.hasArchiveFlagEnabled$ + ? this.i18nService.t("unArchiveAndSave") + : this.i18nService.t("save"); + }); private componentIsDestroyed$ = new Subject(); private allOrganizations: Organization[] = []; @@ -223,11 +250,10 @@ export class VaultV2Component private collectionService: CollectionService, private organizationService: OrganizationService, private folderService: FolderService, - private configService: ConfigService, - private authRequestService: AuthRequestServiceAbstraction, private cipherArchiveService: CipherArchiveService, private policyService: PolicyService, private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} async ngOnInit() { @@ -241,6 +267,7 @@ export class VaultV2Component ) .subscribe((canAccessPremium: boolean) => { this.userHasPremiumAccess = canAccessPremium; + this.userHasPremium.set(canAccessPremium); }); this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { @@ -288,30 +315,40 @@ export class VaultV2Component this.showingModal = false; break; case "copyUsername": { - if (this.cipher?.login?.username) { - this.copyValue(this.cipher, this.cipher?.login?.username, "username", "Username"); + if (this.cipher()?.login?.username) { + this.copyValue( + this.cipher(), + this.cipher()?.login?.username, + "username", + "Username", + ); } break; } case "copyPassword": { - if (this.cipher?.login?.password && this.cipher.viewPassword) { - this.copyValue(this.cipher, this.cipher.login.password, "password", "Password"); + if (this.cipher()?.login?.password && this.cipher().viewPassword) { + this.copyValue( + this.cipher(), + this.cipher().login.password, + "password", + "Password", + ); await this.eventCollectionService - .collect(EventType.Cipher_ClientCopiedPassword, this.cipher.id) + .collect(EventType.Cipher_ClientCopiedPassword, this.cipher().id) .catch(() => {}); } break; } case "copyTotp": { if ( - this.cipher?.login?.hasTotp && - (this.cipher.organizationUseTotp || this.userHasPremiumAccess) + this.cipher()?.login?.hasTotp && + (this.cipher()?.organizationUseTotp || this.userHasPremiumAccess) ) { const value = await firstValueFrom( - this.totpService.getCode$(this.cipher.login.totp), + this.totpService.getCode$(this.cipher()?.login.totp), ).catch((): any => null); if (value) { - this.copyValue(this.cipher, value.code, "verificationCodeTotp", "TOTP"); + this.copyValue(this.cipher(), value.code, "verificationCodeTotp", "TOTP"); } } break; @@ -337,19 +374,12 @@ export class VaultV2Component 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); + await this.sendOpenLoginApprovalMessage(this.activeUserId); + if (this.activeUserId) { this.cipherService .failedToDecryptCiphers$(this.activeUserId) @@ -416,6 +446,7 @@ export class VaultV2Component selectedOrganizationId: params.selectedOrganizationId, myVaultOnly: params.myVaultOnly ?? false, }); + this.activeFilterSubject.next(this.activeFilter); if (this.vaultItemsComponent) { await this.vaultItemsComponent.reload(this.activeFilter.buildFilter()).catch(() => {}); } @@ -440,7 +471,7 @@ export class VaultV2Component return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); this.collections = this.vaultFilterComponent?.collections?.fullList.filter((c) => cipher.collectionIds.includes(c.id), @@ -679,7 +710,7 @@ export class VaultV2Component return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.buildFormConfig("edit"); if (!cipher.edit && this.config) { this.config.mode = "partial-edit"; @@ -693,7 +724,7 @@ export class VaultV2Component return; } this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.buildFormConfig("clone"); this.action = "clone"; await this.go().catch(() => {}); @@ -742,7 +773,7 @@ export class VaultV2Component return; } this.addType = type || this.activeFilter.cipherType; - this.cipher = new CipherView(); + this.cipher.set(new CipherView()); this.cipherId = null; await this.buildFormConfig("add"); this.action = "add"; @@ -774,14 +805,14 @@ export class VaultV2Component ); this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); await this.go().catch(() => {}); await this.vaultItemsComponent?.refresh().catch(() => {}); } async deleteCipher() { this.cipherId = null; - this.cipher = null; + this.cipher.set(null); this.action = null; await this.go().catch(() => {}); await this.vaultItemsComponent?.refresh().catch(() => {}); @@ -796,7 +827,7 @@ export class VaultV2Component async cancelCipher(cipher: CipherView) { this.cipherId = cipher.id; - this.cipher = cipher; + this.cipher.set(cipher); this.action = this.cipherId ? "view" : null; await this.go().catch(() => {}); } @@ -806,6 +837,7 @@ export class VaultV2Component this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter)), ); this.activeFilter = vaultFilter; + this.activeFilterSubject.next(vaultFilter); await this.vaultItemsComponent ?.reload( this.activeFilter.buildFilter(), @@ -887,14 +919,16 @@ export class VaultV2Component /** Refresh the current cipher object */ protected async refreshCurrentCipher() { - if (!this.cipher) { + if (!this.cipher()) { return; } - this.cipher = await firstValueFrom( - this.cipherService.cipherViews$(this.activeUserId!).pipe( - filter((c) => !!c), - map((ciphers) => ciphers.find((c) => c.id === this.cipherId) ?? null), + this.cipher.set( + await firstValueFrom( + this.cipherService.cipherViews$(this.activeUserId!).pipe( + filter((c) => !!c), + map((ciphers) => ciphers.find((c) => c.id === this.cipherId) ?? null), + ), ), ); } @@ -983,9 +1017,7 @@ export class VaultV2Component // clear out organizationId when the user switches to a personal vault filter this.addOrganizationId = null; } - if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { - this.folderId = this.activeFilter.selectedFolderId; - } + this.folderId = this.activeFilter.selectedFolderId; if (this.config == null) { return; @@ -994,6 +1026,8 @@ export class VaultV2Component this.config.initialValues = { ...this.config.initialValues, organizationId: this.addOrganizationId as OrganizationId, + folderId: this.folderId, + collectionIds: this.addCollectionIds as CollectionId[], }; } @@ -1020,4 +1054,27 @@ export class VaultV2Component } return repromptResult; } + + /** + * Sends a message that will retrieve any pending auth requests from the server. If there are + * pending auth requests for this user, a LoginApprovalDialogComponent will open. If there + * are no pending auth requests, nothing happens (see AppComponent: "openLoginApproval"). + */ + private async sendOpenLoginApprovalMessage(activeUserId: UserId) { + // This is a defensive check against a race condition where a user may have successfully logged + // in with no forceSetPasswordReason, but while the vault component is loading, a sync sets + // forceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission (see DefaultSyncService). + // This could potentially happen if an Admin upgrades the user's permissions as the user is logging + // in. In this rare case we do not want to send an "openLoginApproval" message. + // + // This also keeps parity with other usages of the "openLoginApproval" message. That is: don't send + // an "openLoginApproval" message if the user is required to set/change their password. + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(activeUserId), + ); + if (forceSetPasswordReason === ForceSetPasswordReason.None) { + // If there are pending auth requests for this user, a LoginApprovalDialogComponent will open + this.messagingService.send("openLoginApproval"); + } + } } diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index aef8de06968..c779462b500 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -3,6 +3,6 @@ "angularCompilerOptions": { "strictTemplates": true }, - "include": ["src"], + "include": ["src/**/*"], "exclude": ["src/**/*.spec.ts"] } diff --git a/apps/desktop/tsconfig.main.json b/apps/desktop/tsconfig.main.json new file mode 100644 index 00000000000..e30696f1a6d --- /dev/null +++ b/apps/desktop/tsconfig.main.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "moduleResolution": "nodenext", + "module": "nodenext" + }, + "include": ["src/entry.ts", "src/global.d.ts"] +} diff --git a/apps/desktop/tsconfig.preload.json b/apps/desktop/tsconfig.preload.json new file mode 100644 index 00000000000..4ce824e79af --- /dev/null +++ b/apps/desktop/tsconfig.preload.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "include": ["src/preload.ts", "src/global.d.ts"] +} diff --git a/apps/desktop/tsconfig.renderer.json b/apps/desktop/tsconfig.renderer.json index 9a70a18049d..85ee1e3c073 100644 --- a/apps/desktop/tsconfig.renderer.json +++ b/apps/desktop/tsconfig.renderer.json @@ -1,4 +1,7 @@ { - "extends": "./tsconfig.json", - "exclude": ["src/entry.ts", "src/main.ts", "src/main", "src/proxy"] + "extends": "./tsconfig", + "angularCompilerOptions": { + "strictTemplates": true + }, + "include": ["src/app/main.ts", "src/global.d.ts"] } diff --git a/apps/desktop/tsconfig.spec.json b/apps/desktop/tsconfig.spec.json index e6627a8ce45..513cb0beca3 100644 --- a/apps/desktop/tsconfig.spec.json +++ b/apps/desktop/tsconfig.spec.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "./tsconfig", "compilerOptions": { "isolatedModules": true, "emitDecoratorMetadata": false diff --git a/apps/desktop/webpack.base.js b/apps/desktop/webpack.base.js index c9da84cd2e1..b5b73e5167c 100644 --- a/apps/desktop/webpack.base.js +++ b/apps/desktop/webpack.base.js @@ -90,7 +90,10 @@ module.exports.buildConfig = function buildConfig(params) { rules: [ { test: /\.tsx?$/, - use: "ts-loader", + use: { + loader: "ts-loader", + options: { configFile: params.main.tsConfig }, + }, exclude: /node_modules\/(?!(@bitwarden)\/).*/, }, { @@ -148,7 +151,10 @@ module.exports.buildConfig = function buildConfig(params) { rules: [ { test: /\.tsx?$/, - use: "ts-loader", + use: { + loader: "ts-loader", + options: { configFile: params.preload.tsConfig }, + }, exclude: /node_modules\/(?!(@bitwarden)\/).*/, }, ], @@ -296,7 +302,7 @@ module.exports.buildConfig = function buildConfig(params) { }, plugins: [ new AngularWebpackPlugin({ - tsConfigPath: params.renderer.tsConfig, + tsconfig: params.renderer.tsConfig, entryModule: params.renderer.entryModule, sourceMap: true, }), diff --git a/apps/desktop/webpack.config.js b/apps/desktop/webpack.config.js index 685196e56c0..ad21d5e2e9e 100644 --- a/apps/desktop/webpack.config.js +++ b/apps/desktop/webpack.config.js @@ -14,11 +14,11 @@ module.exports = (webpackConfig, context) => { }, main: { entry: path.resolve(__dirname, "src/entry.ts"), - tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.json"), + tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.main.json"), }, preload: { entry: path.resolve(__dirname, "src/preload.ts"), - tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.json"), + tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.preload.json"), }, outputPath: path.resolve(context.context.root, context.options.outputPath), }); @@ -32,11 +32,11 @@ module.exports = (webpackConfig, context) => { }, main: { entry: path.resolve(__dirname, "src/entry.ts"), - tsConfig: path.resolve(__dirname, "tsconfig.json"), + tsConfig: path.resolve(__dirname, "tsconfig.main.json"), }, preload: { entry: path.resolve(__dirname, "src/preload.ts"), - tsConfig: path.resolve(__dirname, "tsconfig.json"), + tsConfig: path.resolve(__dirname, "tsconfig.preload.json"), }, }); } diff --git a/apps/web/package.json b/apps/web/package.json index b92fc5f736a..0e844fbbe79 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.12.2", + "version": "2026.1.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts index b8c82ac2f01..d5d09ad53df 100644 --- a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -7,12 +7,12 @@ import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs"; import { CollectionAdminService, OrganizationUserApiService, - CollectionView, } from "@bitwarden/admin-console/common"; import { getOrganizationById, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts index 70a2e40001a..d10b18b77f6 100644 --- a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CollectionId } from "@bitwarden/sdk-internal"; diff --git a/apps/web/src/app/admin-console/organizations/collections/index.ts b/apps/web/src/app/admin-console/organizations/collections/index.ts index 57f936ab590..e85b3efcab7 100644 --- a/apps/web/src/app/admin-console/organizations/collections/index.ts +++ b/apps/web/src/app/admin-console/organizations/collections/index.ts @@ -1,2 +1 @@ -export * from "./utils"; export * from "./collection-badge"; diff --git a/apps/web/src/app/admin-console/organizations/collections/pipes/get-collection-name.pipe.ts b/apps/web/src/app/admin-console/organizations/collections/pipes/get-collection-name.pipe.ts index b52719304b8..0dfdf6c537b 100644 --- a/apps/web/src/app/admin-console/organizations/collections/pipes/get-collection-name.pipe.ts +++ b/apps/web/src/app/admin-console/organizations/collections/pipes/get-collection-name.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core"; -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; @Pipe({ name: "collectionNameFromId", diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index a253bb87c50..4a973e6fa32 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -15,15 +15,15 @@ import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstraction import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; - -import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; -import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { + VaultFilterServiceAbstraction, VaultFilterList, VaultFilterSection, VaultFilterType, -} from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter-section.type"; -import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type"; + CollectionFilter, +} from "@bitwarden/vault"; + +import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -49,7 +49,7 @@ export class VaultFilterComponent protected destroy$: Subject; constructor( - protected vaultFilterService: VaultFilterService, + protected vaultFilterService: VaultFilterServiceAbstraction, protected policyService: PolicyService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.module.ts index a0dba839b22..97b838216c3 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; import { SearchModule } from "@bitwarden/components"; +import { VaultFilterServiceAbstraction } from "@bitwarden/vault"; -import { VaultFilterService as VaultFilterServiceAbstraction } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilterSharedModule } from "../../../../vault/individual-vault/vault-filter/shared/vault-filter-shared.module"; import { VaultFilterComponent } from "./vault-filter.component"; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts index dc05248d7ba..fa17e243516 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts @@ -1,19 +1,17 @@ import { Injectable, OnDestroy } from "@angular/core"; import { map, Observable, ReplaySubject, Subject } from "rxjs"; -import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; 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 { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections"; 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/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; - -import { VaultFilterService as BaseVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/vault-filter.service"; -import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type"; +import { VaultFilterService as BaseVaultFilterService, CollectionFilter } from "@bitwarden/vault"; @Injectable() export class VaultFilterService extends BaseVaultFilterService implements OnDestroy { @@ -35,7 +33,6 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest stateProvider: StateProvider, collectionService: CollectionService, accountService: AccountService, - configService: ConfigService, ) { super( organizationService, @@ -46,7 +43,6 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest stateProvider, collectionService, accountService, - configService, ); } diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index 30582063ab2..1c9ae89f36b 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -7,12 +7,12 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, switchMap } from "rxjs"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { - CollectionAdminService, CollectionAdminView, Unassigned, -} from "@bitwarden/admin-console/common"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -27,14 +27,10 @@ import { SearchModule, SimpleDialogOptions, } from "@bitwarden/components"; -import { NewCipherMenuComponent } from "@bitwarden/vault"; +import { NewCipherMenuComponent, All, RoutedVaultFilterModel } from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; -import { - All, - RoutedVaultFilterModel, -} from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { CollectionDialogTabType } from "../../shared/components/collection-dialog"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush 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 4adf3739845..5f952fa8b4a 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 @@ -27,19 +27,22 @@ import { takeUntil, } from "rxjs/operators"; -import { - CollectionAdminService, - CollectionAdminView, - CollectionService, - CollectionView, - Unassigned, -} from "@bitwarden/admin-console/common"; +import { CollectionAdminService, CollectionService } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { NoResults } from "@bitwarden/assets/svg"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + CollectionView, + CollectionAdminView, + Unassigned, +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { + getFlatCollectionTree, + getNestedCollectionTree, +} from "@bitwarden/common/admin-console/utils"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; @@ -81,6 +84,13 @@ import { CollectionAssignmentResult, DecryptionFailureDialogComponent, PasswordRepromptService, + VaultFilterServiceAbstraction as VaultFilterService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, + createFilterFunction, + All, + RoutedVaultFilterModel, + VaultFilter, } from "@bitwarden/vault"; import { OrganizationFreeTrialWarningComponent, @@ -102,15 +112,6 @@ import { BulkDeleteDialogResult, openBulkDeleteDialog, } from "../../../vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; -import { VaultFilterService } from "../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; -import { RoutedVaultFilterBridgeService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; -import { createFilterFunction } from "../../../vault/individual-vault/vault-filter/shared/models/filter-function"; -import { - All, - RoutedVaultFilterModel, -} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; -import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { GroupApiService, GroupView } from "../core"; import { openEntityEventsDialog } from "../manage/entity-events.component"; @@ -126,7 +127,6 @@ import { BulkCollectionsDialogResult, } from "./bulk-collections-dialog"; import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component"; -import { getFlatCollectionTree, getNestedCollectionTree } from "./utils"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; diff --git a/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts b/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts index 83fe65c07a9..6c40c4f99a0 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/add-edit-group-detail.ts @@ -1,4 +1,4 @@ -import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; +import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections"; export interface AddEditGroupDetail { id: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts index a72b4c26ebb..35a53b293e8 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/group-details.view.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; +import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections"; import { View } from "@bitwarden/common/models/view/view"; import { GroupDetailsResponse } from "../services/group/responses/group.response"; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts index 264e37c6bd3..0d2fa5b77ce 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts @@ -1,14 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - CollectionAccessSelectionView, - OrganizationUserDetailsResponse, -} from "@bitwarden/admin-console/common"; +import { OrganizationUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; +import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections"; export class OrganizationUserAdminView { id: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 34398728a51..33696af97c4 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -1,14 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - OrganizationUserUserDetailsResponse, - CollectionAccessSelectionView, -} from "@bitwarden/admin-console/common"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; +import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections"; export class OrganizationUserView { id: string; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 03a24703c0f..e778d0fb63d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -18,7 +18,6 @@ import { import { CollectionAdminService, - CollectionAdminView, OrganizationUserApiService, } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -26,6 +25,7 @@ import { getOrganizationById, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index d7dcb8a8aa2..7a103becfad 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -17,15 +17,15 @@ import { } from "rxjs"; import { debounceTime, first } from "rxjs/operators"; +import { CollectionService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { - CollectionService, - CollectionData, - Collection, + CollectionView, CollectionDetailsResponse, CollectionResponse, - CollectionView, -} from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; + Collection, + CollectionData, +} from "@bitwarden/common/admin-console/models/collections"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 3a624e11d95..c95f94b1e11 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -6,7 +6,6 @@ import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { OrganizationUserApiService, - OrganizationUserBulkConfirmRequest, OrganizationUserBulkPublicKeyResponse, OrganizationUserBulkResponse, OrganizationUserService, @@ -15,10 +14,8 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -54,7 +51,6 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { protected i18nService: I18nService, private stateProvider: StateProvider, private organizationUserService: OrganizationUserService, - private configService: ConfigService, ) { super(keyService, encryptService, i18nService); @@ -84,19 +80,9 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { protected postConfirmRequest = async ( userIdsWithKeys: { id: string; key: string }[], ): Promise> => { - if ( - await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) - ) { - return await firstValueFrom( - this.organizationUserService.bulkConfirmUsers(this.organization, userIdsWithKeys), - ); - } else { - const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); - return await this.organizationUserApiService.postOrganizationUserBulkConfirm( - this.organization.id, - request, - ); - } + return await firstValueFrom( + this.organizationUserService.bulkConfirmUsers(this.organization, userIdsWithKeys), + ); }; static open(dialogService: DialogService, config: DialogConfig) { diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 9e40e5afe37..1fa4c8bf8f7 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -15,11 +15,8 @@ import { } from "rxjs"; import { - CollectionAccessSelectionView, CollectionAdminService, - CollectionAdminView, OrganizationUserApiService, - CollectionView, } from "@bitwarden/admin-console/common"; import { getOrganizationById, @@ -30,6 +27,11 @@ import { OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; +import { + CollectionAccessSelectionView, + CollectionAdminView, + CollectionView, +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/web/src/app/admin-console/organizations/members/index.ts b/apps/web/src/app/admin-console/organizations/members/index.ts index 95bd8baf7c7..7026b9bb6ac 100644 --- a/apps/web/src/app/admin-console/organizations/members/index.ts +++ b/apps/web/src/app/admin-console/organizations/members/index.ts @@ -1 +1,2 @@ export * from "./members.module"; +export * from "./pipes"; diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 84e5c33d20d..921004e315d 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -102,15 +102,25 @@ {{ (organization.useGroups ? "groups" : "collections") | i18n }} {{ "role" | i18n }} {{ "policies" | i18n }} - - + +
+ + +
@@ -352,13 +362,16 @@ - +
+
+ +
diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 51a2a6dafc0..e57cf54c180 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -35,6 +35,7 @@ import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billin import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -55,7 +56,11 @@ import { OrganizationUserView } from "../core/views/organization-user.view"; import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; import { MemberDialogResult, MemberDialogTab } from "./components/member-dialog"; -import { MemberDialogManagerService, OrganizationMembersService } from "./services"; +import { + MemberDialogManagerService, + MemberExportService, + OrganizationMembersService, +} from "./services"; import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service"; import { MemberActionsService, @@ -119,6 +124,8 @@ export class MembersComponent extends BaseMembersComponent private policyService: PolicyService, private policyApiService: PolicyApiServiceAbstraction, private organizationMetadataService: OrganizationMetadataServiceAbstraction, + private memberExportService: MemberExportService, + private fileDownloadService: FileDownloadService, private configService: ConfigService, private environmentService: EnvironmentService, ) { @@ -593,4 +600,36 @@ export class MembersComponent extends BaseMembersComponent .getCheckedUsers() .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); } + + exportMembers = async (): Promise => { + try { + const members = this.dataSource.data; + if (!members || members.length === 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noMembersToExport"), + }); + return; + } + + const csvData = this.memberExportService.getMemberExport(members); + const fileName = this.memberExportService.getFileName("org-members"); + + this.fileDownloadService.download({ + fileName: fileName, + blobData: csvData, + blobOptions: { type: "text/plain" }, + }); + + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("dataExportSuccess"), + }); + } catch (e) { + this.validationService.showError(e); + this.logService.error(`Failed to export members: ${e}`); + } + }; } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 3b233932ed3..65625cfd247 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -19,10 +19,12 @@ import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; import { MembersRoutingModule } from "./members-routing.module"; import { MembersComponent } from "./members.component"; +import { UserStatusPipe } from "./pipes"; import { OrganizationMembersService, MemberActionsService, MemberDialogManagerService, + MemberExportService, } from "./services"; @NgModule({ @@ -45,12 +47,15 @@ import { BulkStatusComponent, MembersComponent, BulkDeleteDialogComponent, + UserStatusPipe, ], providers: [ OrganizationMembersService, MemberActionsService, BillingConstraintService, MemberDialogManagerService, + MemberExportService, + UserStatusPipe, ], }) export class MembersModule {} diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/index.ts b/apps/web/src/app/admin-console/organizations/members/pipes/index.ts new file mode 100644 index 00000000000..67c485ed361 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/pipes/index.ts @@ -0,0 +1 @@ +export * from "./user-status.pipe"; diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.spec.ts b/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.spec.ts new file mode 100644 index 00000000000..3fd05c8a2e8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.spec.ts @@ -0,0 +1,47 @@ +import { MockProxy, mock } from "jest-mock-extended"; + +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { UserStatusPipe } from "./user-status.pipe"; + +describe("UserStatusPipe", () => { + let pipe: UserStatusPipe; + let i18nService: MockProxy; + + beforeEach(() => { + i18nService = mock(); + i18nService.t.mockImplementation((key: string) => key); + pipe = new UserStatusPipe(i18nService); + }); + + it("transforms OrganizationUserStatusType.Invited to 'invited'", () => { + expect(pipe.transform(OrganizationUserStatusType.Invited)).toBe("invited"); + expect(i18nService.t).toHaveBeenCalledWith("invited"); + }); + + it("transforms OrganizationUserStatusType.Accepted to 'accepted'", () => { + expect(pipe.transform(OrganizationUserStatusType.Accepted)).toBe("accepted"); + expect(i18nService.t).toHaveBeenCalledWith("accepted"); + }); + + it("transforms OrganizationUserStatusType.Confirmed to 'confirmed'", () => { + expect(pipe.transform(OrganizationUserStatusType.Confirmed)).toBe("confirmed"); + expect(i18nService.t).toHaveBeenCalledWith("confirmed"); + }); + + it("transforms OrganizationUserStatusType.Revoked to 'revoked'", () => { + expect(pipe.transform(OrganizationUserStatusType.Revoked)).toBe("revoked"); + expect(i18nService.t).toHaveBeenCalledWith("revoked"); + }); + + it("transforms null to 'unknown'", () => { + expect(pipe.transform(null)).toBe("unknown"); + expect(i18nService.t).toHaveBeenCalledWith("unknown"); + }); + + it("transforms undefined to 'unknown'", () => { + expect(pipe.transform(undefined)).toBe("unknown"); + expect(i18nService.t).toHaveBeenCalledWith("unknown"); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.ts b/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.ts new file mode 100644 index 00000000000..81590616027 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/pipes/user-status.pipe.ts @@ -0,0 +1,30 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +@Pipe({ + name: "userStatus", + standalone: false, +}) +export class UserStatusPipe implements PipeTransform { + constructor(private i18nService: I18nService) {} + + transform(value?: OrganizationUserStatusType): string { + if (value == null) { + return this.i18nService.t("unknown"); + } + switch (value) { + case OrganizationUserStatusType.Invited: + return this.i18nService.t("invited"); + case OrganizationUserStatusType.Accepted: + return this.i18nService.t("accepted"); + case OrganizationUserStatusType.Confirmed: + return this.i18nService.t("confirmed"); + case OrganizationUserStatusType.Revoked: + return this.i18nService.t("revoked"); + default: + return this.i18nService.t("unknown"); + } + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/index.ts b/apps/web/src/app/admin-console/organizations/members/services/index.ts index baaa33eeae9..fd6b5816513 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/index.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/index.ts @@ -1,4 +1,5 @@ export { OrganizationMembersService } from "./organization-members-service/organization-members.service"; export { MemberActionsService } from "./member-actions/member-actions.service"; export { MemberDialogManagerService } from "./member-dialog-manager/member-dialog-manager.service"; +export { MemberExportService } from "./member-export"; export { DeleteManagedMemberWarningService } from "./delete-managed-member/delete-managed-member-warning.service"; diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts index 80a330b0db1..1dd75a79180 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -12,15 +12,10 @@ import { } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey } from "@bitwarden/common/types/key"; import { newGuid } from "@bitwarden/guid"; -import { KeyService } from "@bitwarden/key-management"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; @@ -30,13 +25,9 @@ describe("MemberActionsService", () => { let service: MemberActionsService; let organizationUserApiService: MockProxy; let organizationUserService: MockProxy; - let keyService: MockProxy; - let encryptService: MockProxy; let configService: MockProxy; - let accountService: FakeAccountService; let organizationMetadataService: MockProxy; - const userId = newGuid() as UserId; const organizationId = newGuid() as OrganizationId; const userIdToManage = newGuid(); @@ -46,10 +37,7 @@ describe("MemberActionsService", () => { beforeEach(() => { organizationUserApiService = mock(); organizationUserService = mock(); - keyService = mock(); - encryptService = mock(); configService = mock(); - accountService = mockAccountServiceWith(userId); organizationMetadataService = mock(); mockOrganization = { @@ -71,10 +59,7 @@ describe("MemberActionsService", () => { service = new MemberActionsService( organizationUserApiService, organizationUserService, - keyService, - encryptService, configService, - accountService, organizationMetadataService, ); }); @@ -242,8 +227,7 @@ describe("MemberActionsService", () => { describe("confirmUser", () => { const publicKey = new Uint8Array([1, 2, 3, 4, 5]); - it("should confirm user using new flow when feature flag is enabled", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); + it("should confirm user", async () => { organizationUserService.confirmUser.mockReturnValue(of(undefined)); const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); @@ -257,44 +241,7 @@ describe("MemberActionsService", () => { expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); }); - it("should confirm user using exising flow when feature flag is disabled", async () => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - - const mockOrgKey = mock(); - const mockOrgKeys = { [organizationId]: mockOrgKey }; - keyService.orgKeys$.mockReturnValue(of(mockOrgKeys)); - - const mockEncryptedKey = new EncString("encrypted-key-data"); - encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncryptedKey); - - organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); - - const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); - - expect(result).toEqual({ success: true }); - expect(keyService.orgKeys$).toHaveBeenCalledWith(userId); - expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(mockOrgKey, publicKey); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( - organizationId, - userIdToManage, - expect.objectContaining({ - key: "encrypted-key-data", - }), - ); - }); - - it("should handle missing organization keys", async () => { - configService.getFeatureFlag$.mockReturnValue(of(false)); - keyService.orgKeys$.mockReturnValue(of({})); - - const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); - - expect(result.success).toBe(false); - expect(result.error).toContain("Organization keys not found"); - }); - it("should handle confirm errors", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); const errorMessage = "Confirm failed"; organizationUserService.confirmUser.mockImplementation(() => { throw new Error(errorMessage); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index f3774e3cb25..a44bfa4b19c 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -1,10 +1,9 @@ import { Injectable } from "@angular/core"; -import { firstValueFrom, switchMap, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { OrganizationUserApiService, OrganizationUserBulkResponse, - OrganizationUserConfirmRequest, OrganizationUserService, } from "@bitwarden/admin-console/common"; import { @@ -12,14 +11,10 @@ import { OrganizationUserStatusType, } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { KeyService } from "@bitwarden/key-management"; import { UserId } from "@bitwarden/user-core"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; @@ -38,15 +33,10 @@ export interface BulkActionResult { @Injectable() export class MemberActionsService { - private userId$ = this.accountService.activeAccount$.pipe(getUserId); - constructor( private organizationUserApiService: OrganizationUserApiService, private organizationUserService: OrganizationUserService, - private keyService: KeyService, - private encryptService: EncryptService, private configService: ConfigService, - private accountService: AccountService, private organizationMetadataService: OrganizationMetadataServiceAbstraction, ) {} @@ -128,37 +118,9 @@ export class MemberActionsService { organization: Organization, ): Promise { try { - if ( - await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) - ) { - await firstValueFrom( - this.organizationUserService.confirmUser(organization, user.id, publicKey), - ); - } else { - const request = await firstValueFrom( - this.userId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - map((orgKeys) => { - if (orgKeys == null || orgKeys[organization.id] == null) { - throw new Error("Organization keys not found for provided User."); - } - return orgKeys[organization.id]; - }), - switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)), - map((encKey) => { - const req = new OrganizationUserConfirmRequest(); - req.key = encKey.encryptedString; - return req; - }), - ), - ); - - await this.organizationUserApiService.postOrganizationUserConfirm( - organization.id, - user.id, - request, - ); - } + await firstValueFrom( + this.organizationUserService.confirmUser(organization, user.id, publicKey), + ); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message ?? String(error) }; diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-export/index.ts b/apps/web/src/app/admin-console/organizations/members/services/member-export/index.ts new file mode 100644 index 00000000000..acd36a91683 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-export/index.ts @@ -0,0 +1,2 @@ +export * from "./member.export"; +export * from "./member-export.service"; diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.spec.ts new file mode 100644 index 00000000000..1e229b95d24 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.spec.ts @@ -0,0 +1,151 @@ +import { TestBed } from "@angular/core/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { UserTypePipe } from "@bitwarden/angular/pipes/user-type.pipe"; +import { + OrganizationUserStatusType, + OrganizationUserType, +} from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { OrganizationUserView } from "../../../core"; +import { UserStatusPipe } from "../../pipes"; + +import { MemberExportService } from "./member-export.service"; + +describe("MemberExportService", () => { + let service: MemberExportService; + let i18nService: MockProxy; + + beforeEach(() => { + i18nService = mock(); + + // Setup common i18n translations + i18nService.t.mockImplementation((key: string) => { + const translations: Record = { + // Column headers + email: "Email", + name: "Name", + status: "Status", + role: "Role", + twoStepLogin: "Two-step Login", + accountRecovery: "Account Recovery", + secretsManager: "Secrets Manager", + groups: "Groups", + // Status values + invited: "Invited", + accepted: "Accepted", + confirmed: "Confirmed", + revoked: "Revoked", + // Role values + owner: "Owner", + admin: "Admin", + user: "User", + custom: "Custom", + // Boolean states + enabled: "Enabled", + disabled: "Disabled", + enrolled: "Enrolled", + notEnrolled: "Not Enrolled", + }; + return translations[key] || key; + }); + + TestBed.configureTestingModule({ + providers: [ + MemberExportService, + { provide: I18nService, useValue: i18nService }, + UserTypePipe, + UserStatusPipe, + ], + }); + + service = TestBed.inject(MemberExportService); + }); + + describe("getMemberExport", () => { + it("should export members with all fields populated", () => { + const members: OrganizationUserView[] = [ + { + email: "user1@example.com", + name: "User One", + status: OrganizationUserStatusType.Confirmed, + type: OrganizationUserType.Admin, + twoFactorEnabled: true, + resetPasswordEnrolled: true, + accessSecretsManager: true, + groupNames: ["Group A", "Group B"], + } as OrganizationUserView, + { + email: "user2@example.com", + name: "User Two", + status: OrganizationUserStatusType.Invited, + type: OrganizationUserType.User, + twoFactorEnabled: false, + resetPasswordEnrolled: false, + accessSecretsManager: false, + groupNames: ["Group C"], + } as OrganizationUserView, + ]; + + const csvData = service.getMemberExport(members); + + expect(csvData).toContain("Email,Name,Status,Role,Two-step Login,Account Recovery"); + expect(csvData).toContain("user1@example.com"); + expect(csvData).toContain("User One"); + expect(csvData).toContain("Confirmed"); + expect(csvData).toContain("Admin"); + expect(csvData).toContain("user2@example.com"); + expect(csvData).toContain("User Two"); + expect(csvData).toContain("Invited"); + }); + + it("should handle members with null name", () => { + const members: OrganizationUserView[] = [ + { + email: "user@example.com", + name: null, + status: OrganizationUserStatusType.Confirmed, + type: OrganizationUserType.User, + twoFactorEnabled: false, + resetPasswordEnrolled: false, + accessSecretsManager: false, + groupNames: [], + } as OrganizationUserView, + ]; + + const csvData = service.getMemberExport(members); + + expect(csvData).toContain("user@example.com"); + // Empty name is represented as an empty field in CSV + expect(csvData).toContain("user@example.com,,Confirmed"); + }); + + it("should handle members with no groups", () => { + const members: OrganizationUserView[] = [ + { + email: "user@example.com", + name: "User", + status: OrganizationUserStatusType.Confirmed, + type: OrganizationUserType.User, + twoFactorEnabled: false, + resetPasswordEnrolled: false, + accessSecretsManager: false, + groupNames: null, + } as OrganizationUserView, + ]; + + const csvData = service.getMemberExport(members); + + expect(csvData).toContain("user@example.com"); + expect(csvData).toBeDefined(); + }); + + it("should handle empty members array", () => { + const csvData = service.getMemberExport([]); + + // When array is empty, papaparse returns an empty string + expect(csvData).toBe(""); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.ts new file mode 100644 index 00000000000..c00881617a4 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-export/member-export.service.ts @@ -0,0 +1,49 @@ +import { inject, Injectable } from "@angular/core"; +import * as papa from "papaparse"; + +import { UserTypePipe } from "@bitwarden/angular/pipes/user-type.pipe"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ExportHelper } from "@bitwarden/vault-export-core"; + +import { OrganizationUserView } from "../../../core"; +import { UserStatusPipe } from "../../pipes"; + +import { MemberExport } from "./member.export"; + +@Injectable() +export class MemberExportService { + private i18nService = inject(I18nService); + private userTypePipe = inject(UserTypePipe); + private userStatusPipe = inject(UserStatusPipe); + + getMemberExport(members: OrganizationUserView[]): string { + const exportData = members.map((m) => + MemberExport.fromOrganizationUserView( + this.i18nService, + this.userTypePipe, + this.userStatusPipe, + m, + ), + ); + + const headers: string[] = [ + this.i18nService.t("email"), + this.i18nService.t("name"), + this.i18nService.t("status"), + this.i18nService.t("role"), + this.i18nService.t("twoStepLogin"), + this.i18nService.t("accountRecovery"), + this.i18nService.t("secretsManager"), + this.i18nService.t("groups"), + ]; + + return papa.unparse(exportData, { + columns: headers, + header: true, + }); + } + + getFileName(prefix: string | null = null, extension = "csv"): string { + return ExportHelper.getFileName(prefix ?? "", extension); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-export/member.export.ts b/apps/web/src/app/admin-console/organizations/members/services/member-export/member.export.ts new file mode 100644 index 00000000000..262e8ebd9fb --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-export/member.export.ts @@ -0,0 +1,43 @@ +import { UserTypePipe } from "@bitwarden/angular/pipes/user-type.pipe"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { OrganizationUserView } from "../../../core"; +import { UserStatusPipe } from "../../pipes"; + +export class MemberExport { + /** + * @param user Organization user to export + * @returns a Record of each column header key, value + * All property members must be a string for export purposes. Null and undefined will appear as + * "null" in a .csv export, therefore an empty string is preferable to a nullish type. + */ + static fromOrganizationUserView( + i18nService: I18nService, + userTypePipe: UserTypePipe, + userStatusPipe: UserStatusPipe, + user: OrganizationUserView, + ): Record { + const result = { + [i18nService.t("email")]: user.email, + [i18nService.t("name")]: user.name ?? "", + [i18nService.t("status")]: userStatusPipe.transform(user.status), + [i18nService.t("role")]: userTypePipe.transform(user.type), + + [i18nService.t("twoStepLogin")]: user.twoFactorEnabled + ? i18nService.t("optionEnabled") + : i18nService.t("disabled"), + + [i18nService.t("accountRecovery")]: user.resetPasswordEnrolled + ? i18nService.t("enrolled") + : i18nService.t("notEnrolled"), + + [i18nService.t("secretsManager")]: user.accessSecretsManager + ? i18nService.t("optionEnabled") + : i18nService.t("disabled"), + + [i18nService.t("groups")]: user.groupNames?.join(", ") ?? "", + }; + + return result; + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts index 0dc417cc2c6..b2695b7568f 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts @@ -1,14 +1,13 @@ import { Injectable } from "@angular/core"; import { combineLatest, firstValueFrom, from, map, switchMap } from "rxjs"; +import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { + CollectionDetailsResponse, Collection, CollectionData, - CollectionDetailsResponse, - CollectionService, - OrganizationUserApiService, -} from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts index 99d484f04f2..9dfb8ebb7e7 100644 --- a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts @@ -22,7 +22,7 @@ import { tap, } from "rxjs"; -import { AutomaticUserConfirmationService } from "@bitwarden/admin-console/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -30,7 +30,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { getById } from "@bitwarden/common/platform/misc"; import { @@ -115,7 +114,6 @@ export class AutoConfirmPolicyDialogComponent formBuilder: FormBuilder, dialogRef: DialogRef, toastService: ToastService, - configService: ConfigService, keyService: KeyService, private organizationService: OrganizationService, private policyService: PolicyService, @@ -131,7 +129,6 @@ export class AutoConfirmPolicyDialogComponent formBuilder, dialogRef, toastService, - configService, keyService, ); diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts index 0e025a9d52a..125876ce05a 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts @@ -188,7 +188,7 @@ describe("PoliciesComponent", () => { }); describe("orgPolicies$", () => { - it("should fetch policies from API for current organization", async () => { + describe("with multiple policies", () => { const mockPolicyResponsesData = [ { id: newGuid(), @@ -206,39 +206,63 @@ describe("PoliciesComponent", () => { }, ]; - const listResponse = new ListResponse( - { Data: mockPolicyResponsesData, ContinuationToken: null }, - PolicyResponse, - ); + beforeEach(async () => { + const listResponse = new ListResponse( + { Data: mockPolicyResponsesData, ContinuationToken: null }, + PolicyResponse, + ); - mockPolicyApiService.getPolicies.mockResolvedValue(listResponse); + mockPolicyApiService.getPolicies.mockResolvedValue(listResponse); - const policies = await firstValueFrom(component["orgPolicies$"]); - expect(policies).toEqual(listResponse.data); - expect(mockPolicyApiService.getPolicies).toHaveBeenCalledWith(mockOrgId); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should fetch policies from API for current organization", async () => { + const policies = await firstValueFrom(component["orgPolicies$"]); + expect(policies.length).toBe(2); + expect(mockPolicyApiService.getPolicies).toHaveBeenCalledWith(mockOrgId); + }); }); - it("should return empty array when API returns no data", async () => { - mockPolicyApiService.getPolicies.mockResolvedValue( - new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse), - ); + describe("with no policies", () => { + beforeEach(async () => { + mockPolicyApiService.getPolicies.mockResolvedValue( + new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse), + ); - const policies = await firstValueFrom(component["orgPolicies$"]); - expect(policies).toEqual([]); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should return empty array when API returns no data", async () => { + const policies = await firstValueFrom(component["orgPolicies$"]); + expect(policies).toEqual([]); + }); }); - it("should return empty array when API returns null data", async () => { - mockPolicyApiService.getPolicies.mockResolvedValue( - new ListResponse({ Data: null, ContinuationToken: null }, PolicyResponse), - ); + describe("with null data", () => { + beforeEach(async () => { + mockPolicyApiService.getPolicies.mockResolvedValue( + new ListResponse({ Data: null, ContinuationToken: null }, PolicyResponse), + ); - const policies = await firstValueFrom(component["orgPolicies$"]); - expect(policies).toEqual([]); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should return empty array when API returns null data", async () => { + const policies = await firstValueFrom(component["orgPolicies$"]); + expect(policies).toEqual([]); + }); }); }); describe("policiesEnabledMap$", () => { - it("should create a map of policy types to their enabled status", async () => { + describe("with multiple policies", () => { const mockPolicyResponsesData = [ { id: "policy-1", @@ -263,27 +287,43 @@ describe("PoliciesComponent", () => { }, ]; - mockPolicyApiService.getPolicies.mockResolvedValue( - new ListResponse( - { Data: mockPolicyResponsesData, ContinuationToken: null }, - PolicyResponse, - ), - ); + beforeEach(async () => { + mockPolicyApiService.getPolicies.mockResolvedValue( + new ListResponse( + { Data: mockPolicyResponsesData, ContinuationToken: null }, + PolicyResponse, + ), + ); - const map = await firstValueFrom(component.policiesEnabledMap$); - expect(map.size).toBe(3); - expect(map.get(PolicyType.TwoFactorAuthentication)).toBe(true); - expect(map.get(PolicyType.RequireSso)).toBe(false); - expect(map.get(PolicyType.SingleOrg)).toBe(true); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create a map of policy types to their enabled status", async () => { + const map = await firstValueFrom(component.policiesEnabledMap$); + expect(map.size).toBe(3); + expect(map.get(PolicyType.TwoFactorAuthentication)).toBe(true); + expect(map.get(PolicyType.RequireSso)).toBe(false); + expect(map.get(PolicyType.SingleOrg)).toBe(true); + }); }); - it("should create empty map when no policies exist", async () => { - mockPolicyApiService.getPolicies.mockResolvedValue( - new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse), - ); + describe("with no policies", () => { + beforeEach(async () => { + mockPolicyApiService.getPolicies.mockResolvedValue( + new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse), + ); - const map = await firstValueFrom(component.policiesEnabledMap$); - expect(map.size).toBe(0); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create empty map when no policies exist", async () => { + const map = await firstValueFrom(component.policiesEnabledMap$); + expect(map.size).toBe(0); + }); }); }); @@ -292,31 +332,36 @@ describe("PoliciesComponent", () => { expect(mockPolicyService.policies$).toHaveBeenCalledWith(mockUserId); }); - it("should refresh policies when policyService emits", async () => { - const policiesSubject = new BehaviorSubject([]); - mockPolicyService.policies$.mockReturnValue(policiesSubject.asObservable()); + describe("when policyService emits", () => { + let policiesSubject: BehaviorSubject; + let callCount: number; - let callCount = 0; - mockPolicyApiService.getPolicies.mockImplementation(() => { - callCount++; - return of(new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse)); + beforeEach(async () => { + policiesSubject = new BehaviorSubject([]); + mockPolicyService.policies$.mockReturnValue(policiesSubject.asObservable()); + + callCount = 0; + mockPolicyApiService.getPolicies.mockImplementation(() => { + callCount++; + return of(new ListResponse({ Data: [], ContinuationToken: null }, PolicyResponse)); + }); + + fixture = TestBed.createComponent(PoliciesComponent); + fixture.detectChanges(); }); - const newFixture = TestBed.createComponent(PoliciesComponent); - newFixture.detectChanges(); + it("should refresh policies when policyService emits", () => { + const initialCallCount = callCount; - const initialCallCount = callCount; + policiesSubject.next([{ type: PolicyType.TwoFactorAuthentication }]); - policiesSubject.next([{ type: PolicyType.TwoFactorAuthentication }]); - - expect(callCount).toBeGreaterThan(initialCallCount); - - newFixture.destroy(); + expect(callCount).toBeGreaterThan(initialCallCount); + }); }); }); describe("handleLaunchEvent", () => { - it("should open policy dialog when policyId is in query params", async () => { + describe("when policyId is in query params", () => { const mockPolicyId = newGuid(); const mockPolicy: BasePolicyEditDefinition = { name: "Test Policy", @@ -335,54 +380,59 @@ describe("PoliciesComponent", () => { data: null, }; - queryParamsSubject.next({ policyId: mockPolicyId }); + let dialogOpenSpy: jest.SpyInstance; - mockPolicyApiService.getPolicies.mockReturnValue( - of( - new ListResponse( - { Data: [mockPolicyResponseData], ContinuationToken: null }, - PolicyResponse, + beforeEach(async () => { + queryParamsSubject.next({ policyId: mockPolicyId }); + + mockPolicyApiService.getPolicies.mockReturnValue( + of( + new ListResponse( + { Data: [mockPolicyResponseData], ContinuationToken: null }, + PolicyResponse, + ), ), - ), - ); + ); - const dialogOpenSpy = jest - .spyOn(PolicyEditDialogComponent, "open") - .mockReturnValue({ close: jest.fn() } as any); + dialogOpenSpy = jest + .spyOn(PolicyEditDialogComponent, "open") + .mockReturnValue({ close: jest.fn() } as any); - TestBed.resetTestingModule(); - await TestBed.configureTestingModule({ - imports: [PoliciesComponent], - providers: [ - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { provide: OrganizationService, useValue: mockOrganizationService }, - { provide: AccountService, useValue: mockAccountService }, - { provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService }, - { provide: PolicyListService, useValue: mockPolicyListService }, - { provide: DialogService, useValue: mockDialogService }, - { provide: PolicyService, useValue: mockPolicyService }, - { provide: ConfigService, useValue: mockConfigService }, - { provide: I18nService, useValue: mockI18nService }, - { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, - { provide: POLICY_EDIT_REGISTER, useValue: [mockPolicy] }, - ], - schemas: [NO_ERRORS_SCHEMA], - }) - .overrideComponent(PoliciesComponent, { - remove: { imports: [] }, - add: { template: "
" }, + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [PoliciesComponent], + providers: [ + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: OrganizationService, useValue: mockOrganizationService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService }, + { provide: PolicyListService, useValue: mockPolicyListService }, + { provide: DialogService, useValue: mockDialogService }, + { provide: PolicyService, useValue: mockPolicyService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: I18nService, useValue: mockI18nService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: POLICY_EDIT_REGISTER, useValue: [mockPolicy] }, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(PoliciesComponent, { + remove: { imports: [] }, + add: { template: "
" }, + }) + .compileComponents(); - const newFixture = TestBed.createComponent(PoliciesComponent); - newFixture.detectChanges(); + fixture = TestBed.createComponent(PoliciesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(dialogOpenSpy).toHaveBeenCalled(); - const callArgs = dialogOpenSpy.mock.calls[0][1]; - expect(callArgs.data?.policy.type).toBe(mockPolicy.type); - expect(callArgs.data?.organizationId).toBe(mockOrgId); - - newFixture.destroy(); + it("should open policy dialog when policyId is in query params", () => { + expect(dialogOpenSpy).toHaveBeenCalled(); + const callArgs = dialogOpenSpy.mock.calls[0][1]; + expect(callArgs.data?.policy.type).toBe(mockPolicy.type); + expect(callArgs.data?.organizationId).toBe(mockOrgId); + }); }); it("should not open dialog when policyId is not in query params", async () => { 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 70daf55f662..1f9a8deaa85 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,7 +1,7 @@ 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 } from "rxjs"; +import { combineLatest, Observable, of, switchMap, first, map, shareReplay } from "rxjs"; 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"; @@ -70,6 +70,7 @@ export class PoliciesComponent { switchMap(() => this.organizationId$), switchMap((organizationId) => this.policyApiService.getPolicies(organizationId)), map((response) => (response.data != null && response.data.length > 0 ? response.data : [])), + shareReplay({ bufferSize: 1, refCount: true }), ); protected policiesEnabledMap$: Observable> = this.orgPolicies$.pipe( 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 ec857478a21..ceecf8f2ecc 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 @@ -1,9 +1,8 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; -import { map, Observable } from "rxjs"; +import { of, Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SharedModule } from "../../../../shared"; @@ -16,9 +15,8 @@ export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { component = OrganizationDataOwnershipPolicyComponent; display$(organization: Organization, configService: ConfigService): Observable { - return configService - .getFeatureFlag$(FeatureFlag.CreateDefaultLocation) - .pipe(map((enabled) => !enabled)); + // TODO Remove this entire component upon verifying that it can be deleted due to its sole reliance of the CreateDefaultLocation feature flag + return of(false); } } 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 ed26dd37801..59670457d88 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 @@ -1,12 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } from "@angular/core"; -import { lastValueFrom, Observable } from "rxjs"; +import { lastValueFrom } 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 { 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"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrgKey } from "@bitwarden/common/types/key"; import { CenterPositionStrategy, DialogService } from "@bitwarden/components"; @@ -28,10 +25,6 @@ export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefiniti type = PolicyType.OrganizationDataOwnership; component = vNextOrganizationDataOwnershipPolicyComponent; showDescription = false; - - override display$(organization: Organization, configService: ConfigService): Observable { - return configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation); - } } @Component({ diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts index 98b6d1c6bee..c633ff5f421 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts @@ -14,8 +14,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { @@ -75,7 +73,6 @@ export class PolicyEditDialogComponent implements AfterViewInit { private formBuilder: FormBuilder, protected dialogRef: DialogRef, protected toastService: ToastService, - private configService: ConfigService, private keyService: KeyService, ) {} @@ -132,10 +129,7 @@ export class PolicyEditDialogComponent implements AfterViewInit { } try { - if ( - this.policyComponent instanceof vNextOrganizationDataOwnershipPolicyComponent && - (await this.isVNextEnabled()) - ) { + if (this.policyComponent instanceof vNextOrganizationDataOwnershipPolicyComponent) { await this.handleVNextSubmission(this.policyComponent); } else { await this.handleStandardSubmission(); @@ -154,14 +148,6 @@ export class PolicyEditDialogComponent implements AfterViewInit { } }; - private async isVNextEnabled(): Promise { - const isVNextFeatureEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation), - ); - - return isVNextFeatureEnabled; - } - private async handleStandardSubmission(): Promise { if (!this.policyComponent) { throw new Error("PolicyComponent not initialized."); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index 45691ae98d6..9755beefbf1 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,13 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - CollectionAccessSelectionView, - OrganizationUserUserDetailsResponse, -} from "@bitwarden/admin-console/common"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; +import { CollectionAccessSelectionView } from "@bitwarden/common/admin-console/models/collections"; import { SelectItemView } from "@bitwarden/components"; import { GroupView } from "../../../core"; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 7b189270e1b..4f40ea701d2 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -18,19 +18,21 @@ import { import { first } from "rxjs/operators"; import { - CollectionAccessSelectionView, CollectionAdminService, - CollectionAdminView, OrganizationUserApiService, OrganizationUserUserMiniResponse, - CollectionResponse, - CollectionView, CollectionService, } from "@bitwarden/admin-console/common"; import { getOrganizationById, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + CollectionAccessSelectionView, + CollectionAdminView, + CollectionView, + CollectionResponse, +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts index 7132428c375..32a5736748c 100644 --- a/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts +++ b/apps/web/src/app/admin-console/organizations/shared/validators/free-org-collection-limit.validator.ts @@ -1,7 +1,7 @@ import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms"; import { combineLatest, map, Observable, of } from "rxjs"; -import { Collection } from "@bitwarden/admin-console/common"; +import { Collection } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { getById } from "@bitwarden/common/platform/misc"; diff --git a/apps/web/src/app/billing/clients/account-billing.client.ts b/apps/web/src/app/billing/clients/account-billing.client.ts index 256a06b3ead..e520e70bf70 100644 --- a/apps/web/src/app/billing/clients/account-billing.client.ts +++ b/apps/web/src/app/billing/clients/account-billing.client.ts @@ -1,6 +1,8 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BitwardenSubscriptionResponse } from "@bitwarden/common/billing/models/response/bitwarden-subscription.response"; +import { BitwardenSubscription } from "@bitwarden/subscription"; import { BillingAddress, @@ -11,13 +13,22 @@ import { @Injectable() export class AccountBillingClient { private endpoint = "/account/billing/vnext"; - private apiService: ApiService; - constructor(apiService: ApiService) { - this.apiService = apiService; - } + constructor(private apiService: ApiService) {} - purchasePremiumSubscription = async ( + getLicense = async (): Promise => { + const path = `${this.endpoint}/license`; + return this.apiService.send("GET", path, null, true, true); + }; + + getSubscription = async (): Promise => { + const path = `${this.endpoint}/subscription`; + const json = await this.apiService.send("GET", path, null, true, true); + const response = new BitwardenSubscriptionResponse(json); + return response.toDomain(); + }; + + purchaseSubscription = async ( paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod, billingAddress: Pick, ): Promise => { @@ -29,6 +40,17 @@ export class AccountBillingClient { const request = isTokenizedPayment ? { tokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress } : { nonTokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress }; + await this.apiService.send("POST", path, request, true, true); }; + + reinstateSubscription = async (): Promise => { + const path = `${this.endpoint}/subscription/reinstate`; + await this.apiService.send("POST", path, null, true, false); + }; + + updateSubscriptionStorage = async (additionalStorageGb: number): Promise => { + const path = `${this.endpoint}/subscription/storage`; + await this.apiService.send("PUT", path, { additionalStorageGb }, true, false); + }; } diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index fbaf65d1839..f85dab54fe7 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -1,9 +1,12 @@ import { inject, NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component"; import { SelfHostedPremiumComponent } from "@bitwarden/web-vault/app/billing/individual/premium/self-hosted-premium.component"; +import { AccountSubscriptionComponent } from "@bitwarden/web-vault/app/billing/individual/subscription/account-subscription.component"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component"; @@ -17,11 +20,15 @@ const routes: Routes = [ data: { titleId: "subscription" }, children: [ { path: "", pathMatch: "full", redirectTo: "premium" }, - { - path: "user-subscription", - component: UserSubscriptionComponent, - data: { titleId: "premiumMembership" }, - }, + ...featureFlaggedRoute({ + defaultComponent: UserSubscriptionComponent, + flaggedComponent: AccountSubscriptionComponent, + featureFlag: FeatureFlag.PM29594_UpdateIndividualSubscriptionPage, + routeOptions: { + path: "user-subscription", + data: { titleId: "premiumMembership" }, + }, + }), /** * Two-Route Matching Strategy for /premium: * diff --git a/apps/web/src/app/billing/individual/subscription/account-subscription.component.html b/apps/web/src/app/billing/individual/subscription/account-subscription.component.html new file mode 100644 index 00000000000..9bb788c1f36 --- /dev/null +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.html @@ -0,0 +1,50 @@ +@if (subscriptionLoading()) { + + + {{ "loading" | i18n }} + +} @else { + @if (subscription.value(); as subscription) { + +
+

{{ "youHavePremium" | i18n }}

+

+ {{ "viewAndManagePremiumSubscription" | i18n }} +

+
+ + +
+ + + + + @if (subscription.storage; as storage) { + + } + + + +
+ } +} 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 new file mode 100644 index 00000000000..183f4f82666 --- /dev/null +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts @@ -0,0 +1,291 @@ +import { ChangeDetectionStrategy, Component, computed, inject, resource } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, lastValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction"; +import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService, TypographyModule } from "@bitwarden/components"; +import { Maybe } from "@bitwarden/pricing"; +import { + AdditionalOptionsCardAction, + AdditionalOptionsCardActions, + AdditionalOptionsCardComponent, + MAX_STORAGE_GB, + Storage, + StorageCardAction, + StorageCardActions, + StorageCardComponent, + SubscriptionCardAction, + SubscriptionCardActions, + SubscriptionCardComponent, + SubscriptionStatuses, +} from "@bitwarden/subscription"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients"; +import { + AdjustAccountSubscriptionStorageDialogComponent, + AdjustAccountSubscriptionStorageDialogParams, +} from "@bitwarden/web-vault/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component"; +import { + OffboardingSurveyDialogResultType, + openOffboardingSurvey, +} from "@bitwarden/web-vault/app/billing/shared/offboarding-survey.component"; + +@Component({ + templateUrl: "./account-subscription.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AdditionalOptionsCardComponent, + I18nPipe, + JslibModule, + StorageCardComponent, + SubscriptionCardComponent, + TypographyModule, + ], + providers: [AccountBillingClient], +}) +export class AccountSubscriptionComponent { + private accountService = inject(AccountService); + private activatedRoute = inject(ActivatedRoute); + private accountBillingClient = inject(AccountBillingClient); + private billingAccountProfileStateService = inject(BillingAccountProfileStateService); + private configService = inject(ConfigService); + private dialogService = inject(DialogService); + private fileDownloadService = inject(FileDownloadService); + private i18nService = inject(I18nService); + private router = inject(Router); + private subscriptionPricingService = inject(SubscriptionPricingServiceAbstraction); + private toastService = inject(ToastService); + + readonly subscription = resource({ + loader: async () => { + const redirectToPremiumPage = async (): Promise => { + await this.router.navigate(["/settings/subscription/premium"]); + return null; + }; + const account = await firstValueFrom(this.accountService.activeAccount$); + if (!account) { + return await redirectToPremiumPage(); + } + const hasPremiumPersonally = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ); + if (!hasPremiumPersonally) { + return await redirectToPremiumPage(); + } + return await this.accountBillingClient.getSubscription(); + }, + }); + + readonly subscriptionLoading = computed(() => this.subscription.isLoading()); + + readonly subscriptionTerminal = computed>(() => { + const subscription = this.subscription.value(); + if (subscription) { + return ( + subscription.status === SubscriptionStatuses.IncompleteExpired || + subscription.status === SubscriptionStatuses.Canceled || + subscription.status === SubscriptionStatuses.Unpaid + ); + } + }); + + readonly subscriptionPendingCancellation = computed>(() => { + const subscription = this.subscription.value(); + if (subscription) { + return ( + (subscription.status === SubscriptionStatuses.Trialing || + subscription.status === SubscriptionStatuses.Active) && + !!subscription.cancelAt + ); + } + }); + + readonly storage = computed>(() => { + const subscription = this.subscription.value(); + return subscription?.storage; + }); + + readonly purchasedStorage = computed(() => { + const subscription = this.subscription.value(); + return subscription?.cart.passwordManager.additionalStorage?.quantity; + }); + + readonly premiumPlan = toSignal( + this.subscriptionPricingService + .getPersonalSubscriptionPricingTiers$() + .pipe( + map((tiers) => + tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium), + ), + ), + ); + + readonly premiumStoragePrice = computed>(() => { + const premiumPlan = this.premiumPlan(); + return premiumPlan?.passwordManager.annualPricePerAdditionalStorageGB; + }); + + readonly premiumProvidedStorage = computed>(() => { + const premiumPlan = this.premiumPlan(); + return premiumPlan?.passwordManager.providedStorageGB; + }); + + readonly canAddStorage = computed>(() => { + if (this.subscriptionTerminal()) { + return false; + } + const storage = this.storage(); + const premiumProvidedStorage = this.premiumProvidedStorage(); + if (storage && premiumProvidedStorage) { + const maxAttainableStorage = MAX_STORAGE_GB - premiumProvidedStorage; + return storage.available < maxAttainableStorage; + } + }); + + readonly canRemoveStorage = computed>(() => { + if (this.subscriptionTerminal()) { + return false; + } + const purchasedStorage = this.purchasedStorage(); + if (!purchasedStorage || purchasedStorage === 0) { + return false; + } + const storage = this.storage(); + if (storage) { + return storage.available > storage.used; + } + }); + + readonly canCancelSubscription = computed>(() => { + if (this.subscriptionTerminal()) { + return false; + } + return !this.subscriptionPendingCancellation(); + }); + + readonly premiumToOrganizationUpgradeEnabled = toSignal( + this.configService.getFeatureFlag$(FeatureFlag.PM29593_PremiumToOrganizationUpgrade), + { initialValue: false }, + ); + + onSubscriptionCardAction = async (action: SubscriptionCardAction) => { + switch (action) { + case SubscriptionCardActions.ContactSupport: + window.open("https://bitwarden.com/contact/", "_blank"); + break; + case SubscriptionCardActions.ManageInvoices: + await this.router.navigate(["../billing-history"], { relativeTo: this.activatedRoute }); + break; + case SubscriptionCardActions.ReinstateSubscription: { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "reinstateSubscription" }, + content: { key: "reinstateConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + await this.accountBillingClient.reinstateSubscription(); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("reinstated"), + }); + this.subscription.reload(); + break; + } + case SubscriptionCardActions.UpdatePayment: + await this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }); + break; + case SubscriptionCardActions.UpgradePlan: + // TODO: Implement upgrade plan navigation + break; + } + }; + + onStorageCardAction = async (action: StorageCardAction) => { + const data = this.getAdjustStorageDialogParams(action); + const dialogReference = AdjustAccountSubscriptionStorageDialogComponent.open( + this.dialogService, + { + data, + }, + ); + const result = await lastValueFrom(dialogReference.closed); + if (result === "submitted") { + this.subscription.reload(); + } + }; + + onAdditionalOptionsCardAction = async (action: AdditionalOptionsCardAction) => { + switch (action) { + case AdditionalOptionsCardActions.DownloadLicense: { + const license = await this.accountBillingClient.getLicense(); + const json = JSON.stringify(license, null, 2); + this.fileDownloadService.download({ + fileName: "bitwarden_premium_license.json", + blobData: json, + }); + break; + } + case AdditionalOptionsCardActions.CancelSubscription: { + const dialogReference = openOffboardingSurvey(this.dialogService, { + data: { + type: "User", + }, + }); + + const result = await lastValueFrom(dialogReference.closed); + + if (result === OffboardingSurveyDialogResultType.Closed) { + return; + } + + this.subscription.reload(); + } + } + }; + + getAdjustStorageDialogParams = ( + action: StorageCardAction, + ): Maybe => { + const purchasedStorage = this.purchasedStorage(); + const storagePrice = this.premiumStoragePrice(); + const providedStorage = this.premiumProvidedStorage(); + + switch (action) { + case StorageCardActions.AddStorage: { + if (storagePrice && providedStorage) { + return { + type: "add", + price: storagePrice, + provided: providedStorage, + cadence: "annually", + existing: purchasedStorage, + }; + } + break; + } + case StorageCardActions.RemoveStorage: { + if (purchasedStorage) { + return { + type: "remove", + existing: purchasedStorage, + }; + } + break; + } + } + }; +} diff --git a/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.html b/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.html new file mode 100644 index 00000000000..1d3172837ad --- /dev/null +++ b/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.html @@ -0,0 +1,43 @@ +@let content = this.content(); +
+ + +

{{ content.body }}

+
+ + {{ content.label }} + + @if (action() === "add") { + + + {{ "total" | i18n }} + {{ formGroup.value.amount }} GB × {{ price() | currency: "$" }} = + {{ price() * formGroup.value.amount | currency: "$" }} / + {{ term() | i18n }} + + } + +
+
+ + + + +
+
diff --git a/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.ts b/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.ts new file mode 100644 index 00000000000..f1350cda49e --- /dev/null +++ b/apps/web/src/app/billing/individual/subscription/adjust-account-subscription-storage-dialog.component.ts @@ -0,0 +1,182 @@ +import { CurrencyPipe } from "@angular/common"; +import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core"; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + AsyncActionsModule, + ButtonModule, + DIALOG_DATA, + DialogConfig, + DialogModule, + DialogRef, + DialogService, + FormFieldModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; +import { Maybe } from "@bitwarden/pricing"; +import { MAX_STORAGE_GB } from "@bitwarden/subscription"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients"; + +type RemoveStorage = { + type: "remove"; + existing: number; +}; + +type AddStorage = { + type: "add"; + price: number; + provided: number; + cadence: SubscriptionCadence; + existing?: number; +}; + +export type AdjustAccountSubscriptionStorageDialogParams = RemoveStorage | AddStorage; + +type AdjustAccountSubscriptionStorageDialogResult = "closed" | "submitted"; + +@Component({ + templateUrl: "./adjust-account-subscription-storage-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [AccountBillingClient], + imports: [ + AsyncActionsModule, + ButtonModule, + CurrencyPipe, + DialogModule, + FormFieldModule, + I18nPipe, + ReactiveFormsModule, + TypographyModule, + ], +}) +export class AdjustAccountSubscriptionStorageDialogComponent { + private readonly accountBillingClient = inject(AccountBillingClient); + private readonly dialogParams = inject(DIALOG_DATA); + private readonly dialogRef = inject(DialogRef); + private readonly i18nService = inject(I18nService); + private readonly toastService = inject(ToastService); + + readonly action = computed<"add" | "remove">(() => this.dialogParams.type); + + readonly price = computed>(() => { + if (this.dialogParams.type === "add") { + return this.dialogParams.price; + } + }); + + readonly provided = computed>(() => { + if (this.dialogParams.type === "add") { + return this.dialogParams.provided; + } + }); + + readonly term = computed>(() => { + if (this.dialogParams.type === "add") { + switch (this.dialogParams.cadence) { + case "annually": + return this.i18nService.t("year"); + case "monthly": + return this.i18nService.t("month"); + } + } + }); + + readonly existing = computed>(() => this.dialogParams.existing); + + readonly content = computed<{ + title: string; + body: string; + label: string; + }>(() => { + const action = this.action(); + switch (action) { + case "add": + return { + title: this.i18nService.t("addStorage"), + body: this.i18nService.t("storageAddNote"), + label: this.i18nService.t("gbStorageAdd"), + }; + case "remove": + return { + title: this.i18nService.t("removeStorage"), + body: this.i18nService.t("whenYouRemoveStorage"), + label: this.i18nService.t("gbStorageRemove"), + }; + } + }); + + readonly maxPurchasable = computed>(() => { + const provided = this.provided(); + if (provided) { + return MAX_STORAGE_GB - provided; + } + }); + + readonly maxValidatorValue = computed(() => { + const maxPurchasable = this.maxPurchasable() ?? MAX_STORAGE_GB; + const existing = this.existing(); + const action = this.action(); + + switch (action) { + case "add": { + return existing ? maxPurchasable - existing : maxPurchasable; + } + case "remove": { + return existing ? existing : 0; + } + } + }); + + formGroup = new FormGroup({ + amount: new FormControl(1, { + nonNullable: true, + validators: [ + Validators.required, + Validators.min(1), + Validators.max(this.maxValidatorValue()), + ], + }), + }); + + submit = async () => { + this.formGroup.markAllAsTouched(); + if (!this.formGroup.valid || !this.formGroup.value.amount) { + return; + } + + const action = this.action(); + const existing = this.existing(); + const amount = this.formGroup.value.amount; + + switch (action) { + case "add": { + await this.accountBillingClient.updateSubscriptionStorage(amount + (existing ?? 0)); + break; + } + case "remove": { + await this.accountBillingClient.updateSubscriptionStorage(existing! - amount); + } + } + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("adjustedStorage", amount), + }); + + this.dialogRef.close("submitted"); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open( + AdjustAccountSubscriptionStorageDialogComponent, + dialogConfig, + ); +} diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts index b18e3a7f5c3..3ba2f634785 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts @@ -5,8 +5,6 @@ import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-pro import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; @@ -26,7 +24,6 @@ import { describe("UnifiedUpgradePromptService", () => { let sut: UnifiedUpgradePromptService; const mockAccountService = mock(); - const mockConfigService = mock(); const mockBillingService = mock(); const mockVaultProfileService = mock(); const mockSyncService = mock(); @@ -59,7 +56,6 @@ describe("UnifiedUpgradePromptService", () => { function setupTestService() { sut = new UnifiedUpgradePromptService( mockAccountService, - mockConfigService, mockBillingService, mockVaultProfileService, mockSyncService, @@ -80,7 +76,6 @@ describe("UnifiedUpgradePromptService", () => { beforeEach(() => { mockAccountService.activeAccount$ = accountSubject.asObservable(); mockPlatformUtilsService.isSelfHost.mockReturnValue(false); - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockStateProvider.getUserState$.mockReturnValue(of(false)); setupTestService(); @@ -96,7 +91,6 @@ describe("UnifiedUpgradePromptService", () => { mockAccountService.activeAccount$ = accountSubject.asObservable(); mockDialogOpen.mockReset(); mockReset(mockDialogService); - mockReset(mockConfigService); mockReset(mockBillingService); mockReset(mockVaultProfileService); mockReset(mockSyncService); @@ -112,11 +106,10 @@ describe("UnifiedUpgradePromptService", () => { mockStateProvider.getUserState$.mockReturnValue(of(false)); mockStateProvider.setUserState.mockResolvedValue(undefined); }); - it("should subscribe to account and feature flag observables when checking display conditions", async () => { + it("should subscribe to account observables when checking display conditions", async () => { // Arrange mockPlatformUtilsService.isSelfHost.mockReturnValue(false); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); - mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); setupTestService(); @@ -125,34 +118,12 @@ describe("UnifiedUpgradePromptService", () => { await sut.displayUpgradePromptConditionally(); // Assert - expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog, - ); expect(mockAccountService.activeAccount$).toBeDefined(); }); - it("should not show dialog when feature flag is disabled", async () => { - // Arrange - mockPlatformUtilsService.isSelfHost.mockReturnValue(false); - mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); - mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); - mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); - const recentDate = new Date(); - recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old - mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); - - setupTestService(); - // Act - const result = await sut.displayUpgradePromptConditionally(); - - // Assert - expect(result).toBeNull(); - expect(mockDialogOpen).not.toHaveBeenCalled(); - }); it("should not show dialog when user has premium", async () => { // Arrange mockPlatformUtilsService.isSelfHost.mockReturnValue(false); - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(true)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); setupTestService(); @@ -167,7 +138,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when user has any organization membership", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([{ id: "org1" } as any])); mockPlatformUtilsService.isSelfHost.mockReturnValue(false); @@ -183,7 +153,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when profile is older than 5 minutes", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const oldDate = new Date(); @@ -202,7 +171,6 @@ describe("UnifiedUpgradePromptService", () => { it("should show dialog when all conditions are met", async () => { //Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); @@ -224,7 +192,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when account is null/undefined", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); accountSubject.next(null); // Set account to null setupTestService(); @@ -238,7 +205,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when profile creation date is unavailable", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockVaultProfileService.getProfileCreationDate.mockResolvedValue(null); @@ -256,7 +222,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when running in self-hosted environment", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); const recentDate = new Date(); @@ -275,7 +240,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not show dialog when user has previously dismissed the modal", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); @@ -295,7 +259,6 @@ describe("UnifiedUpgradePromptService", () => { it("should save dismissal state when user closes the dialog", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); @@ -320,7 +283,6 @@ describe("UnifiedUpgradePromptService", () => { it("should not save dismissal state when user upgrades to premium", async () => { // Arrange - mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts index 3ea8f19341d..f5a32483a4d 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts @@ -6,8 +6,6 @@ import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-pro import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; @@ -38,7 +36,6 @@ export class UnifiedUpgradePromptService { private unifiedUpgradeDialogRef: DialogRef | null = null; constructor( private accountService: AccountService, - private configService: ConfigService, private billingAccountProfileStateService: BillingAccountProfileStateService, private vaultProfileService: VaultProfileService, private syncService: SyncService, @@ -70,26 +67,13 @@ export class UnifiedUpgradePromptService { isProfileLessThanFiveMinutesOld$, hasOrganizations$, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), - this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog), hasDismissedModal$, ]).pipe( - map( - ([ - isProfileLessThanFiveMinutesOld, - hasOrganizations, - hasPremium, - isFlagEnabled, - hasDismissed, - ]) => { - return ( - isProfileLessThanFiveMinutesOld && - !hasOrganizations && - !hasPremium && - isFlagEnabled && - !hasDismissed - ); - }, - ), + map(([isProfileLessThanFiveMinutesOld, hasOrganizations, hasPremium, hasDismissed]) => { + return ( + isProfileLessThanFiveMinutesOld && !hasOrganizations && !hasPremium && !hasDismissed + ); + }), ); }), take(1), diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts index 81169d719b6..83440646b48 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts @@ -478,13 +478,13 @@ describe("UpgradePaymentService", () => { describe("upgradeToPremium", () => { it("should call accountBillingClient to purchase premium subscription and refresh data", async () => { // Arrange - mockAccountBillingClient.purchasePremiumSubscription.mockResolvedValue(); + mockAccountBillingClient.purchaseSubscription.mockResolvedValue(); // Act await sut.upgradeToPremium(mockTokenizedPaymentMethod, mockBillingAddress); // Assert - expect(mockAccountBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith( + expect(mockAccountBillingClient.purchaseSubscription).toHaveBeenCalledWith( mockTokenizedPaymentMethod, mockBillingAddress, ); @@ -496,13 +496,13 @@ describe("UpgradePaymentService", () => { const accountCreditPaymentMethod: NonTokenizedPaymentMethod = { type: NonTokenizablePaymentMethods.accountCredit, }; - mockAccountBillingClient.purchasePremiumSubscription.mockResolvedValue(); + mockAccountBillingClient.purchaseSubscription.mockResolvedValue(); // Act await sut.upgradeToPremium(accountCreditPaymentMethod, mockBillingAddress); // Assert - expect(mockAccountBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith( + expect(mockAccountBillingClient.purchaseSubscription).toHaveBeenCalledWith( accountCreditPaymentMethod, mockBillingAddress, ); diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts index ae18ab4c629..b8d5637e471 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts @@ -143,7 +143,7 @@ export class UpgradePaymentService { ): Promise { this.validatePaymentAndBillingInfo(paymentMethod, billingAddress); - await this.accountBillingClient.purchasePremiumSubscription(paymentMethod, billingAddress); + await this.accountBillingClient.purchaseSubscription(paymentMethod, billingAddress); await this.refreshAndSync(); } diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html index 45a68136a00..02dd69be6aa 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html @@ -50,11 +50,7 @@
- + @if (isFamiliesPlan) {

{{ "paymentChargedWithTrial" | i18n }} diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index a824e850db6..77ae3b31837 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -9,7 +9,7 @@ import { signal, viewChild, } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { debounceTime, @@ -22,6 +22,7 @@ import { combineLatest, map, shareReplay, + defer, } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; @@ -35,7 +36,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { ButtonModule, DialogModule, ToastService } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; -import { CartSummaryComponent } from "@bitwarden/pricing"; +import { Cart, CartSummaryComponent } from "@bitwarden/pricing"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { @@ -118,23 +119,48 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { protected readonly selectedPlan = signal(null); protected readonly loading = signal(true); protected readonly upgradeToMessage = signal(""); - // Cart Summary data - protected readonly passwordManager = computed(() => { - if (!this.selectedPlan()) { - return { name: "", cost: 0, quantity: 0, cadence: "year" as const }; - } - - return { - name: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership", - cost: this.selectedPlan()!.details.passwordManager.annualPrice, - quantity: 1, - cadence: "year" as const, - }; - }); protected hasEnoughAccountCredit$!: Observable; private pricingTiers$!: Observable; - protected estimatedTax$!: Observable; + + // Use defer to lazily create the observable when subscribed to + protected estimatedTax$ = defer(() => + this.formGroup.controls.billingAddress.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.value), + debounceTime(1000), + switchMap(() => this.refreshSalesTax$()), + ), + ); + + // Convert estimatedTax$ to signal for use in computed cart + protected readonly estimatedTax = toSignal(this.estimatedTax$, { + initialValue: this.INITIAL_TAX_VALUE, + }); + + // Cart Summary data + protected readonly cart = computed(() => { + if (!this.selectedPlan()) { + return { + passwordManager: { + seats: { translationKey: "", cost: 0, quantity: 0 }, + }, + cadence: "annually", + estimatedTax: 0, + }; + } + + return { + passwordManager: { + seats: { + translationKey: this.isFamiliesPlan ? "familiesMembership" : "premiumMembership", + cost: this.selectedPlan()!.details.passwordManager.annualPrice ?? 0, + quantity: 1, + }, + }, + cadence: "annually", + estimatedTax: this.estimatedTax() ?? 0, + }; + }); constructor( private i18nService: I18nService, @@ -186,13 +212,6 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { } }); - this.estimatedTax$ = this.formGroup.controls.billingAddress.valueChanges.pipe( - startWith(this.formGroup.controls.billingAddress.value), - debounceTime(1000), - // Only proceed when form has required values - switchMap(() => this.refreshSalesTax$()), - ); - this.loading.set(false); } diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index 2d653ff200b..f060d29b377 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -40,13 +40,13 @@

{{ "status" | i18n }}
-
+
{{ (subscription && subscriptionStatus) || "-" }} {{ "pendingCancellation" | i18n }}
@@ -65,7 +65,7 @@ }}
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 8d99b807540..5034b21d03d 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { DiscountInfo } from "@bitwarden/pricing"; +import { Discount, DiscountTypes, Maybe } from "@bitwarden/pricing"; import { AdjustStorageDialogComponent, @@ -30,6 +30,7 @@ import { import { UpdateLicenseDialogComponent } from "../shared/update-license-dialog.component"; import { UpdateLicenseDialogResult } from "../shared/update-license-types"; +// TODO: Remove with deletion of pm-29594-update-individual-subscription-page // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -251,15 +252,13 @@ export class UserSubscriptionComponent implements OnInit { } } - getDiscountInfo(discount: BillingCustomerDiscount | null): DiscountInfo | null { + getDiscount(discount: BillingCustomerDiscount | null): Maybe { if (!discount) { return null; } - return { - active: discount.active, - percentOff: discount.percentOff, - amountOff: discount.amountOff, - }; + return discount.amountOff + ? { type: DiscountTypes.AmountOff, value: discount.amountOff } + : { type: DiscountTypes.PercentOff, value: discount.percentOff }; } get isSubscriptionActive(): boolean { diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 860f80eb346..4858deabec6 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -40,21 +40,27 @@ {{ i.amount | currency: "$" }} - + {{ "freeForOneYear" | i18n }}
- {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} + {{ i.quantity * i.amount | currency: "$" }} / + {{ i.interval | i18n }} {{ - calculateTotalAppliedDiscount(i.quantity * i.amount) | currency: "$" - }} - / {{ "year" | i18n }}{{ i.quantity * i.originalAmount | currency: "$" }} / + {{ "year" | i18n }}
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index de5d71cce5e..323a190fe1c 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -19,11 +19,9 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response"; -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 { DialogService, ToastService } from "@bitwarden/components"; @@ -82,9 +80,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, - private configService: ConfigService, private toastService: ToastService, - private billingApiService: BillingApiServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, ) {} @@ -218,6 +214,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy get subscriptionLineItems() { return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({ name: lineItem.name, + originalAmount: lineItem.amount, amount: this.discountPrice(lineItem.amount, lineItem.productId), quantity: lineItem.quantity, interval: lineItem.interval, @@ -406,12 +403,16 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy const isSmStandalone = this.sub?.customerDiscount?.id === "sm-standalone"; const appliesToProduct = this.sub?.subscription?.items?.some((item) => - this.sub?.customerDiscount?.appliesTo?.includes(item.productId), + this.discountAppliesToProduct(item.productId), ) ?? false; return isSmStandalone && appliesToProduct; } + discountAppliesToProduct(productId: string): boolean { + return this.sub?.customerDiscount?.appliesTo?.includes(productId) ?? false; + } + closeChangePlan() { this.showChangePlan = false; } @@ -438,10 +439,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy await this.load(); } - calculateTotalAppliedDiscount(total: number) { - return total / (1 - this.customerDiscount?.percentOff / 100); - } - adjustStorage = (add: boolean) => { return async () => { const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index bd557dc5947..661d14502fe 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -9,8 +9,6 @@ import { DefaultCollectionAdminService, OrganizationUserApiService, CollectionService, - AutomaticUserConfirmationService, - DefaultAutomaticUserConfirmationService, OrganizationUserService, DefaultOrganizationUserService, } from "@bitwarden/admin-console/common"; @@ -46,6 +44,10 @@ 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 { @@ -59,9 +61,11 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-auth-request-answering.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"; @@ -374,6 +378,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, InternalOrganizationServiceAbstraction, OrganizationUserApiService, + PolicyService, ], }), safeProvider({ @@ -483,6 +488,11 @@ const safeProviders: SafeProvider[] = [ useClass: SessionTimeoutSettingsComponentService, deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyService], }), + safeProvider({ + provide: AuthRequestAnsweringService, + useClass: NoopAuthRequestAnsweringService, + deps: [], + }), ]; @NgModule({ diff --git a/apps/web/src/app/dirt/reports/pages/breach-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/breach-report.component.spec.ts index 886267e3189..6a26d8abd74 100644 --- a/apps/web/src/app/dirt/reports/pages/breach-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/breach-report.component.spec.ts @@ -1,17 +1,17 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { Component, ChangeDetectionStrategy } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ReactiveFormsModule } from "@angular/forms"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BreachAccountResponse } from "@bitwarden/common/dirt/models/response/breach-account.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { mockAccountInfoWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { AsyncActionsModule, ButtonModule, FormFieldModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BreachReportComponent } from "./breach-report.component"; @@ -32,6 +32,21 @@ const breachedAccounts = [ }), ]; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-header", + template: "
", + standalone: false, +}) +class MockHeaderComponent {} +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "bit-container", + template: "
", + standalone: false, +}) +class MockBitContainerComponent {} + describe("BreachReportComponent", () => { let component: BreachReportComponent; let fixture: ComponentFixture; @@ -51,8 +66,8 @@ describe("BreachReportComponent", () => { accountService.activeAccount$ = activeAccountSubject; await TestBed.configureTestingModule({ - declarations: [BreachReportComponent, I18nPipe], - imports: [ReactiveFormsModule], + declarations: [BreachReportComponent, MockHeaderComponent, MockBitContainerComponent], + imports: [ReactiveFormsModule, I18nPipe, AsyncActionsModule, ButtonModule, FormFieldModule], providers: [ { provide: AuditService, @@ -67,9 +82,7 @@ describe("BreachReportComponent", () => { useValue: mock(), }, ], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownElements: false, - errorOnUnknownProperties: false, + schemas: [], }).compileComponents(); }); diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts index 560245bdc34..e056ec44af5 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts @@ -1,8 +1,8 @@ +import { Component, ChangeDetectionStrategy } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -12,7 +12,13 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { + DialogService, + AsyncActionsModule, + ButtonModule, + FormFieldModule, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -20,6 +26,22 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component"; import { cipherData } from "./reports-ciphers.mock"; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-header", + template: "
", + standalone: false, +}) +class MockHeaderComponent {} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "bit-container", + template: "
", + standalone: false, +}) +class MockBitContainerComponent {} + describe("ExposedPasswordsReportComponent", () => { let component: ExposedPasswordsReportComponent; let fixture: ComponentFixture; @@ -30,16 +52,19 @@ describe("ExposedPasswordsReportComponent", () => { const userId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(userId); - beforeEach(() => { + beforeEach(async () => { let cipherFormConfigServiceMock: MockProxy; syncServiceMock = mock(); auditService = mock(); organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - declarations: [ExposedPasswordsReportComponent, I18nPipe], + await TestBed.configureTestingModule({ + declarations: [ + ExposedPasswordsReportComponent, + MockHeaderComponent, + MockBitContainerComponent, + ], + imports: [I18nPipe, AsyncActionsModule, ButtonModule, FormFieldModule], providers: [ { provide: CipherService, @@ -83,9 +108,6 @@ describe("ExposedPasswordsReportComponent", () => { }, ], schemas: [], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownElements: false, - errorOnUnknownProperties: false, }).compileComponents(); }); diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts index 64a851e120e..12453ea3b88 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts @@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } 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"; @@ -14,6 +13,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -37,7 +37,8 @@ describe("InactiveTwoFactorReportComponent", () => { syncServiceMock = mock(); await TestBed.configureTestingModule({ - declarations: [InactiveTwoFactorReportComponent, I18nPipe], + declarations: [InactiveTwoFactorReportComponent], + imports: [I18nPipe], providers: [ { provide: CipherService, diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index f83614557bd..1d3d8d71f5a 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -17,14 +17,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault"; +import { + PasswordRepromptService, + CipherFormConfigService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../exposed-passwords-report.component"; diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts index b1adbd26eb3..23d1330dad7 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts @@ -12,14 +12,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { + CipherFormConfigService, + PasswordRepromptService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../inactive-two-factor-report.component"; diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 3944e2edfcb..599774d5515 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -16,14 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { + CipherFormConfigService, + PasswordRepromptService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } from "../reused-passwords-report.component"; diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index d49baa5d465..6bf741b86eb 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -16,14 +16,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { + CipherFormConfigService, + PasswordRepromptService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent } from "../unsecured-websites-report.component"; diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index 5158416dd28..6780b65931c 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -17,14 +17,17 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; -import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { + CipherFormConfigService, + PasswordRepromptService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../../../layouts/header/header.module"; import { SharedModule } from "../../../../shared"; import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from "../weak-passwords-report.component"; diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts index 5933d2ce293..1b7006d0c68 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.spec.ts @@ -1,8 +1,8 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } 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"; @@ -12,6 +12,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -19,6 +20,22 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { cipherData } from "./reports-ciphers.mock"; import { ReusedPasswordsReportComponent } from "./reused-passwords-report.component"; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-header", + template: "
", + standalone: false, +}) +class MockHeaderComponent {} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "bit-container", + template: "
", + standalone: false, +}) +class MockBitContainerComponent {} + describe("ReusedPasswordsReportComponent", () => { let component: ReusedPasswordsReportComponent; let fixture: ComponentFixture; @@ -28,15 +45,18 @@ describe("ReusedPasswordsReportComponent", () => { const userId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(userId); - beforeEach(() => { + beforeEach(async () => { let cipherFormConfigServiceMock: MockProxy; organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - declarations: [ReusedPasswordsReportComponent, I18nPipe], + await TestBed.configureTestingModule({ + declarations: [ + ReusedPasswordsReportComponent, + MockHeaderComponent, + MockBitContainerComponent, + ], + imports: [I18nPipe], providers: [ { provide: CipherService, @@ -76,8 +96,6 @@ describe("ReusedPasswordsReportComponent", () => { }, ], schemas: [], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownElements: false, }).compileComponents(); }); diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts index 040d73a0d66..2107e0c8df7 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.spec.ts @@ -1,9 +1,9 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } 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"; @@ -13,6 +13,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -20,6 +21,22 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { cipherData } from "./reports-ciphers.mock"; import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.component"; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-header", + template: "
", + standalone: false, +}) +class MockHeaderComponent {} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "bit-container", + template: "
", + standalone: false, +}) +class MockBitContainerComponent {} + describe("UnsecuredWebsitesReportComponent", () => { let component: UnsecuredWebsitesReportComponent; let fixture: ComponentFixture; @@ -30,7 +47,7 @@ describe("UnsecuredWebsitesReportComponent", () => { const userId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(userId); - beforeEach(() => { + beforeEach(async () => { let cipherFormConfigServiceMock: MockProxy; organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); @@ -38,10 +55,13 @@ describe("UnsecuredWebsitesReportComponent", () => { collectionService = mock(); adminConsoleCipherFormConfigService = mock(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - declarations: [UnsecuredWebsitesReportComponent, I18nPipe], + await TestBed.configureTestingModule({ + declarations: [ + UnsecuredWebsitesReportComponent, + MockHeaderComponent, + MockBitContainerComponent, + ], + imports: [I18nPipe], providers: [ { provide: CipherService, @@ -85,8 +105,6 @@ describe("UnsecuredWebsitesReportComponent", () => { }, ], schemas: [], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownElements: false, }).compileComponents(); }); diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts index d78dc7e3ceb..a63723dc688 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.spec.ts @@ -1,8 +1,8 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; -import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { OrganizationService } 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"; @@ -13,6 +13,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -20,6 +21,22 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { cipherData } from "./reports-ciphers.mock"; import { WeakPasswordsReportComponent } from "./weak-passwords-report.component"; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-header", + template: "
", + standalone: false, +}) +class MockHeaderComponent {} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "bit-container", + template: "
", + standalone: false, +}) +class MockBitContainerComponent {} + describe("WeakPasswordsReportComponent", () => { let component: WeakPasswordsReportComponent; let fixture: ComponentFixture; @@ -30,16 +47,16 @@ describe("WeakPasswordsReportComponent", () => { const userId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(userId); - beforeEach(() => { + beforeEach(async () => { let cipherFormConfigServiceMock: MockProxy; syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - TestBed.configureTestingModule({ - declarations: [WeakPasswordsReportComponent, I18nPipe], + + await TestBed.configureTestingModule({ + declarations: [WeakPasswordsReportComponent, MockHeaderComponent, MockBitContainerComponent], + imports: [I18nPipe], providers: [ { provide: CipherService, @@ -84,8 +101,6 @@ describe("WeakPasswordsReportComponent", () => { }, ], schemas: [], - // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports - errorOnUnknownElements: false, }).compileComponents(); }); diff --git a/apps/web/src/app/dirt/reports/reports.module.ts b/apps/web/src/app/dirt/reports/reports.module.ts index 358768e71ee..5648b40982a 100644 --- a/apps/web/src/app/dirt/reports/reports.module.ts +++ b/apps/web/src/app/dirt/reports/reports.module.ts @@ -1,14 +1,17 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault"; +import { + CipherFormConfigService, + DefaultCipherFormConfigService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, +} from "@bitwarden/vault"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; -import { RoutedVaultFilterBridgeService } from "../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { BreachReportComponent } from "./pages/breach-report.component"; diff --git a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts index 9ae0600fb2a..7d6ad41cb00 100644 --- a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts +++ b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.spec.ts @@ -122,6 +122,41 @@ describe("CipherStep", () => { expect(logger.record).toHaveBeenCalledWith("Cipher ID cipher-3 was undecryptable"); expect(logger.record).toHaveBeenCalledWith("Found 2 undecryptable ciphers"); }); + + it("returns correct results when running diagnostics multiple times", async () => { + const userId = "user-id" as UserId; + const cipher1 = { id: "cipher-1", organizationId: null } as Cipher; + const cipher2 = { id: "cipher-2", organizationId: null } as Cipher; + + const workingData: RecoveryWorkingData = { + userId, + userKey: null, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [cipher1, cipher2], + folders: [], + }; + + // First run: cipher1 succeeds, cipher2 fails + cipherEncryptionService.decrypt + .mockResolvedValueOnce({} as any) + .mockRejectedValueOnce(new Error("Decryption failed")); + + const result1 = await cipherStep.runDiagnostics(workingData, logger); + + expect(result1).toBe(false); + expect(cipherStep.canRecover(workingData)).toBe(true); + + // Second run: all ciphers succeed + cipherEncryptionService.decrypt.mockResolvedValue({} as any); + + const result2 = await cipherStep.runDiagnostics(workingData, logger); + + expect(result2).toBe(true); + expect(cipherStep.canRecover(workingData)).toBe(false); + expect(cipherStep["undecryptableCipherIds"]).toHaveLength(0); + expect(cipherStep["decryptableCipherIds"]).toHaveLength(2); + }); }); describe("canRecover", () => { diff --git a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts index 01c2d9bc2a1..47000f8880b 100644 --- a/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts +++ b/apps/web/src/app/key-management/data-recovery/steps/cipher-step.ts @@ -25,6 +25,7 @@ export class CipherStep implements RecoveryStep { } this.undecryptableCipherIds = []; + this.decryptableCipherIds = []; // The tool is currently only implemented to handle ciphers that are corrupt for a user. For an organization, the case of // local user not having access to the organization key is not properly handled here, and should be implemented separately. // For now, this just filters out and does not consider corrupt organization ciphers. diff --git a/apps/web/src/app/key-management/data-recovery/steps/folder-step.spec.ts b/apps/web/src/app/key-management/data-recovery/steps/folder-step.spec.ts new file mode 100644 index 00000000000..8e59007732e --- /dev/null +++ b/apps/web/src/app/key-management/data-recovery/steps/folder-step.spec.ts @@ -0,0 +1,404 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { UserKey } from "@bitwarden/common/types/key"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; +import { DialogService } from "@bitwarden/components"; +import { PureCrypto } from "@bitwarden/sdk-internal"; +import { UserId } from "@bitwarden/user-core"; + +import { LogRecorder } from "../log-recorder"; + +import { FolderStep } from "./folder-step"; +import { RecoveryWorkingData } from "./recovery-step"; + +// Mock SdkLoadService +jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk-load.service", () => ({ + SdkLoadService: { + Ready: Promise.resolve(), + }, +})); + +jest.mock("@bitwarden/sdk-internal", () => ({ + PureCrypto: { + symmetric_decrypt_string: jest.fn(), + }, +})); + +describe("FolderStep", () => { + let folderStep: FolderStep; + let folderService: MockProxy; + let dialogService: MockProxy; + let logger: MockProxy; + + const mockUserKey = { + toEncoded: jest.fn().mockReturnValue("encoded-user-key"), + } as unknown as UserKey; + + beforeEach(() => { + folderService = mock(); + dialogService = mock(); + logger = mock(); + + folderStep = new FolderStep(folderService, dialogService); + + jest.clearAllMocks(); + }); + + describe("runDiagnostics", () => { + it("returns false and logs error when userKey is missing", async () => { + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: null, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [], + }; + + const result = await folderStep.runDiagnostics(workingData, logger); + + expect(result).toBe(false); + expect(logger.record).toHaveBeenCalledWith("Missing user key"); + }); + + it("returns true when all folders are decryptable", async () => { + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }; + + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockReturnValue("decrypted-name"); + + const result = await folderStep.runDiagnostics(workingData, logger); + + expect(result).toBe(true); + expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith( + "encrypted-name-1", + "encoded-user-key", + ); + expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith( + "encrypted-name-2", + "encoded-user-key", + ); + expect(logger.record).toHaveBeenCalledWith("Found 0 undecryptable folders"); + expect(logger.record).toHaveBeenCalledWith("Found 2 decryptable folders"); + }); + + it("returns false and records folders with no name", async () => { + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: null as null }; + const folder3 = { id: "folder-3", name: { encryptedString: null as null } }; + + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2, folder3] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockReturnValue("decrypted-name"); + + const result = await folderStep.runDiagnostics(workingData, logger); + + expect(result).toBe(false); + expect(logger.record).toHaveBeenCalledWith("Folder ID folder-2 has no name"); + expect(logger.record).toHaveBeenCalledWith("Folder ID folder-3 has no name"); + expect(logger.record).toHaveBeenCalledWith("Found 2 undecryptable folders"); + expect(logger.record).toHaveBeenCalledWith("Found 1 decryptable folders"); + }); + + it("returns false and records undecryptable folders", async () => { + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }; + const folder3 = { id: "folder-3", name: { encryptedString: "encrypted-name-3" } }; + + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2, folder3] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock) + .mockReturnValueOnce("decrypted-name") // folder1 succeeds + .mockImplementationOnce(() => { + throw new Error("Decryption failed"); + }) // folder2 fails + .mockImplementationOnce(() => { + throw new Error("Decryption failed"); + }); // folder3 fails + + const result = await folderStep.runDiagnostics(workingData, logger); + + expect(result).toBe(false); + expect(logger.record).toHaveBeenCalledWith( + "Folder name for folder ID folder-2 was undecryptable", + ); + expect(logger.record).toHaveBeenCalledWith( + "Folder name for folder ID folder-3 was undecryptable", + ); + expect(logger.record).toHaveBeenCalledWith("Found 2 undecryptable folders"); + expect(logger.record).toHaveBeenCalledWith("Found 1 decryptable folders"); + }); + + it("returns correct results when running diagnostics multiple times", async () => { + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }; + + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2] as Folder[], + }; + + // First run: folder1 succeeds, folder2 fails + (PureCrypto.symmetric_decrypt_string as jest.Mock) + .mockReturnValueOnce("decrypted-name") + .mockImplementationOnce(() => { + throw new Error("Decryption failed"); + }); + + const result1 = await folderStep.runDiagnostics(workingData, logger); + + expect(result1).toBe(false); + expect(folderStep.canRecover(workingData)).toBe(true); + + // Second run: all folders succeed + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockReturnValue("decrypted-name"); + + const result2 = await folderStep.runDiagnostics(workingData, logger); + + expect(result2).toBe(true); + expect(folderStep.canRecover(workingData)).toBe(false); + expect(folderStep["undecryptableFolderIds"]).toEqual([]); + expect(folderStep["decryptableFolderIds"]).toEqual(["folder-1", "folder-2"]); + }); + }); + + describe("canRecover", () => { + it("returns false when there are no undecryptable folders", async () => { + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [ + { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }, + { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }, + ] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockReturnValue("decrypted-name"); + + await folderStep.runDiagnostics(workingData, logger); + const result = folderStep.canRecover(workingData); + + expect(result).toBe(false); + }); + + it("returns true when there are undecryptable folders but at least one decryptable folder", async () => { + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [ + { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }, + { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }, + ] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock) + .mockReturnValueOnce("decrypted-name") + .mockImplementationOnce(() => { + throw new Error("Decryption failed"); + }); + + await folderStep.runDiagnostics(workingData, logger); + const result = folderStep.canRecover(workingData); + + expect(result).toBe(true); + }); + + it("returns false when all folders are undecryptable", async () => { + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [ + { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }, + { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }, + ] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockImplementation(() => { + throw new Error("Decryption failed"); + }); + + await folderStep.runDiagnostics(workingData, logger); + const result = folderStep.canRecover(workingData); + + expect(result).toBe(false); + }); + }); + + describe("runRecovery", () => { + it("logs and returns early when there are no undecryptable folders", async () => { + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [], + }; + + await folderStep.runRecovery(workingData, logger); + + expect(logger.record).toHaveBeenCalledWith("No undecryptable folders to recover"); + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(folderService.delete).not.toHaveBeenCalled(); + }); + + it("throws error when userId is missing", async () => { + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + + const workingData: RecoveryWorkingData = { + userId: "user-id" as UserId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockImplementation(() => { + throw new Error("Decryption failed"); + }); + await folderStep.runDiagnostics(workingData, logger); + + // Now set userId to null for recovery + workingData.userId = null; + + await expect(folderStep.runRecovery(workingData, logger)).rejects.toThrow("Missing user ID"); + expect(logger.record).toHaveBeenCalledWith("Missing user ID"); + }); + + it("throws error when user cancels deletion", async () => { + const userId = "user-id" as UserId; + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + + const workingData: RecoveryWorkingData = { + userId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockImplementation(() => { + throw new Error("Decryption failed"); + }); + await folderStep.runDiagnostics(workingData, logger); + + dialogService.openSimpleDialog.mockResolvedValue(false); + + await expect(folderStep.runRecovery(workingData, logger)).rejects.toThrow( + "Folder recovery cancelled by user", + ); + + expect(logger.record).toHaveBeenCalledWith("Showing confirmation dialog for 1 folders"); + expect(logger.record).toHaveBeenCalledWith("User cancelled folder deletion"); + expect(folderService.delete).not.toHaveBeenCalled(); + }); + + it("deletes undecryptable folders when user confirms", async () => { + const userId = "user-id" as UserId; + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }; + + const workingData: RecoveryWorkingData = { + userId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockImplementation(() => { + throw new Error("Decryption failed"); + }); + await folderStep.runDiagnostics(workingData, logger); + + dialogService.openSimpleDialog.mockResolvedValue(true); + folderService.delete.mockResolvedValue(undefined); + + await folderStep.runRecovery(workingData, logger); + + expect(logger.record).toHaveBeenCalledWith("Showing confirmation dialog for 2 folders"); + expect(logger.record).toHaveBeenCalledWith("Deleting 2 folders"); + expect(folderService.delete).toHaveBeenCalledWith("folder-1", userId); + expect(folderService.delete).toHaveBeenCalledWith("folder-2", userId); + expect(logger.record).toHaveBeenCalledWith("Deleted folder folder-1"); + expect(logger.record).toHaveBeenCalledWith("Deleted folder folder-2"); + expect(logger.record).toHaveBeenCalledWith("Successfully deleted 2 folders"); + }); + + it("continues deleting folders even if some deletions fail", async () => { + const userId = "user-id" as UserId; + const folder1 = { id: "folder-1", name: { encryptedString: "encrypted-name-1" } }; + const folder2 = { id: "folder-2", name: { encryptedString: "encrypted-name-2" } }; + const folder3 = { id: "folder-3", name: { encryptedString: "encrypted-name-3" } }; + + const workingData: RecoveryWorkingData = { + userId, + userKey: mockUserKey, + encryptedPrivateKey: null, + isPrivateKeyCorrupt: false, + ciphers: [], + folders: [folder1, folder2, folder3] as Folder[], + }; + + (PureCrypto.symmetric_decrypt_string as jest.Mock).mockImplementation(() => { + throw new Error("Decryption failed"); + }); + await folderStep.runDiagnostics(workingData, logger); + + dialogService.openSimpleDialog.mockResolvedValue(true); + folderService.delete + .mockResolvedValueOnce(undefined) // folder1 succeeds + .mockRejectedValueOnce(new Error("Network error")) // folder2 fails + .mockResolvedValueOnce(undefined); // folder3 succeeds + + await folderStep.runRecovery(workingData, logger); + + expect(folderService.delete).toHaveBeenCalledTimes(3); + expect(logger.record).toHaveBeenCalledWith("Deleted folder folder-1"); + expect(logger.record).toHaveBeenCalledWith( + "Failed to delete folder folder-2: Error: Network error", + ); + expect(logger.record).toHaveBeenCalledWith("Deleted folder folder-3"); + }); + }); +}); diff --git a/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts b/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts index 90e252ce6c3..2087c360ddc 100644 --- a/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts +++ b/apps/web/src/app/key-management/data-recovery/steps/folder-step.ts @@ -25,6 +25,8 @@ export class FolderStep implements RecoveryStep { } this.undecryptableFolderIds = []; + this.decryptableFolderIds = []; + for (const folder of workingData.folders) { if (!folder.name?.encryptedString) { logger.record(`Folder ID ${folder.id} has no name`); diff --git a/apps/web/src/app/key-management/data-recovery/steps/private-key-step.ts b/apps/web/src/app/key-management/data-recovery/steps/private-key-step.ts index 82c20c466b8..45c2c6e8a93 100644 --- a/apps/web/src/app/key-management/data-recovery/steps/private-key-step.ts +++ b/apps/web/src/app/key-management/data-recovery/steps/private-key-step.ts @@ -85,9 +85,14 @@ export class PrivateKeyStep implements RecoveryStep { } logger.record("Replacing private key"); - await this.privateKeyRegenerationService.regenerateUserPublicKeyEncryptionKeyPair( - workingData.userId!, - ); - logger.record("Private key replaced successfully"); + const recovered = + await this.privateKeyRegenerationService.regenerateUserPublicKeyEncryptionKeyPair( + workingData.userId!, + ); + if (!recovered) { + logger.record("Private key replacement could not be performed"); + } else { + logger.record("Private key replacement replaced successfully"); + } } } diff --git a/apps/web/src/app/layouts/navbar.component.html b/apps/web/src/app/layouts/navbar.component.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index f7f319f2fab..b46629bc6a8 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -54,6 +54,7 @@ describe("ProductSwitcherService", () => { platformUtilsService = mock(); billingAccountProfileStateService = mock(); configService = mock(); + configService.getFeatureFlag$.mockReturnValue(of(false)); router.url = "/"; router.events = of({}); 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 6cfecd59403..31fcf9ffe6d 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 @@ -19,7 +19,11 @@ import { } 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"; -import { PolicyType, ProviderType } from "@bitwarden/common/admin-console/enums"; +import { + OrganizationUserType, + PolicyType, + ProviderType, +} from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -155,14 +159,16 @@ export class ProductSwitcherService { this.userHasSingleOrgPolicy$, this.route.paramMap, this.triggerProductUpdate$, + this.configService.getFeatureFlag$(FeatureFlag.SM1719_RemoveSecretsManagerAds), ]).pipe( map( - ([orgs, providers, userHasSingleOrgPolicy, paramMap]: [ + ([orgs, providers, userHasSingleOrgPolicy, paramMap, , removeSecretsManagerAdsFlag]: [ 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)); @@ -208,6 +214,15 @@ export class ProductSwitcherService { external: false, }; + // 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, + ); + const products = { pm: { name: "Password Manager", @@ -267,7 +282,8 @@ export class ProductSwitcherService { if (smOrg) { bento.push(products.sm); - } else { + } else if (!shouldDisableSMAds) { + // Only show SM in "other" section if ads are not disabled other.push(products.sm); } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 3af514466b7..90207f59ad4 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -4,12 +4,12 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit, Signal } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { RouterModule } from "@angular/router"; -import { combineLatest, map, Observable, switchMap } from "rxjs"; +import { Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PasswordManagerLogo } from "@bitwarden/assets/svg"; +import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -58,21 +58,11 @@ export class UserLayoutComponent implements OnInit { ); this.showEmergencyAccess = toSignal( - combineLatest([ - this.configService.getFeatureFlag$(FeatureFlag.AutoConfirm), - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => - this.policyService.policyAppliesToUser$(PolicyType.AutoConfirm, userId), - ), + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + canAccessEmergencyAccess(userId, this.configService, this.policyService), ), - ]).pipe( - map(([enabled, policyAppliesToUser]) => { - if (!enabled || !policyAppliesToUser) { - return true; - } - return false; - }), ), ); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index f4fd55bd1e6..932d0b8119b 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; import { Route, RouterModule, Routes } from "@angular/router"; +import { organizationPolicyGuard } from "@bitwarden/angular/admin-console/guards"; import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component"; import { AuthRoute } from "@bitwarden/angular/auth/constants"; import { @@ -56,7 +57,6 @@ import { premiumInterestRedirectGuard } from "@bitwarden/web-vault/app/vault/gua import { flagEnabled, Flags } from "../utils/flags"; -import { organizationPolicyGuard } from "./admin-console/organizations/guards/org-policy.guard"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; diff --git a/apps/web/src/app/tools/event-export/event-export.service.ts b/apps/web/src/app/tools/event-export/event-export.service.ts index f39b786b6d1..d888af51edf 100644 --- a/apps/web/src/app/tools/event-export/event-export.service.ts +++ b/apps/web/src/app/tools/event-export/event-export.service.ts @@ -4,6 +4,7 @@ import { Injectable } from "@angular/core"; import * as papa from "papaparse"; import { EventView } from "@bitwarden/common/models/view/event.view"; +import { ExportHelper } from "@bitwarden/vault-export-core"; import { EventExport } from "./event.export"; @@ -16,25 +17,6 @@ export class EventExportService { } getFileName(prefix: string = null, extension = "csv"): string { - const now = new Date(); - const dateString = - now.getFullYear() + - "" + - this.padNumber(now.getMonth() + 1, 2) + - "" + - this.padNumber(now.getDate(), 2) + - this.padNumber(now.getHours(), 2) + - "" + - this.padNumber(now.getMinutes(), 2) + - this.padNumber(now.getSeconds(), 2); - - return "bitwarden" + (prefix ? "_" + prefix : "") + "_export_" + dateString + "." + extension; - } - - private padNumber(num: number, width: number, padCharacter = "0"): string { - const numString = num.toString(); - return numString.length >= width - ? numString - : new Array(width - numString.length + 1).join(padCharacter) + numString; + return ExportHelper.getFileName(prefix ?? "", extension); } } diff --git a/apps/web/src/app/tools/import/import-collection-admin.service.ts b/apps/web/src/app/tools/import/import-collection-admin.service.ts index b63cd15047b..1f77d99cace 100644 --- a/apps/web/src/app/tools/import/import-collection-admin.service.ts +++ b/apps/web/src/app/tools/import/import-collection-admin.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; import { UserId } from "@bitwarden/user-core"; diff --git a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts index 4f5dda1745e..e9ef85867e7 100644 --- a/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts +++ b/apps/web/src/app/tools/send/new-send/new-send-dropdown.component.spec.ts @@ -8,9 +8,9 @@ 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { SendAddEditDialogComponent } from "@bitwarden/send-ui"; @@ -72,6 +72,7 @@ describe("NewSendDropdownComponent", () => { const openSpy = jest.spyOn(SendAddEditDialogComponent, "open"); const openDrawerSpy = jest.spyOn(SendAddEditDialogComponent, "openDrawer"); mockConfigService.getFeatureFlag.mockResolvedValue(false); + openSpy.mockReturnValue({ closed: of({}) } as any); await component.createSend(SendType.Text); @@ -85,6 +86,8 @@ describe("NewSendDropdownComponent", () => { mockConfigService.getFeatureFlag.mockImplementation(async (key) => key === FeatureFlag.SendUIRefresh ? true : false, ); + const mockRef = { closed: of({}) }; + openDrawerSpy.mockReturnValue(mockRef as any); await component.createSend(SendType.Text); 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 22f07e4fe92..68c8c188d31 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,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { firstValueFrom, Observable, of, switchMap } from "rxjs"; +import { firstValueFrom, Observable, of, switchMap, lastValueFrom } from "rxjs"; import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -8,9 +8,15 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; 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/enums/send-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components"; -import { DefaultSendFormConfigService, SendAddEditDialogComponent } from "@bitwarden/send-ui"; +import { + DefaultSendFormConfigService, + SendAddEditDialogComponent, + SendItemDialogResult, +} from "@bitwarden/send-ui"; + +import { SendSuccessDrawerDialogComponent } from "../shared"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -60,12 +66,19 @@ export class NewSendDropdownComponent { if (!(await firstValueFrom(this.canAccessPremium$)) && type === SendType.File) { return; } - const formConfig = await this.addEditFormConfigService.buildConfig("add", undefined, type); - const useRefresh = await this.configService.getFeatureFlag(FeatureFlag.SendUIRefresh); + if (useRefresh) { - SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig }); + const dialogRef = SendAddEditDialogComponent.openDrawer(this.dialogService, { formConfig }); + if (dialogRef) { + const result = await lastValueFrom(dialogRef.closed); + if (result?.result === SendItemDialogResult.Saved && result?.send) { + this.dialogService.openDrawer(SendSuccessDrawerDialogComponent, { + data: result.send, + }); + } + } } else { SendAddEditDialogComponent.open(this.dialogService, { formConfig }); } diff --git a/apps/web/src/app/tools/send/send-access/access.component.html b/apps/web/src/app/tools/send/send-access/access.component.html index aec6e2a10b9..b86933410b8 100644 --- a/apps/web/src/app/tools/send/send-access/access.component.html +++ b/apps/web/src/app/tools/send/send-access/access.component.html @@ -1,52 +1,14 @@ -
- - {{ "viewSendHiddenEmailWarning" | i18n }} - {{ - "learnMore" | i18n - }}. - - - -
-

{{ "sendAccessUnavailable" | i18n }}

-
-
-

{{ "unexpectedErrorSend" | i18n }}

-
-
-

- {{ send.name }} -

-
- - - - - - - - -

- Expires: {{ expirationDate | date: "medium" }} -

-
-
- -
- - {{ "loading" | i18n }} -
-
-
+@switch (viewState) { + @case ("auth") { + + } + @case ("view") { + + } +} diff --git a/apps/web/src/app/tools/send/send-access/access.component.ts b/apps/web/src/app/tools/send/send-access/access.component.ts index 273f1c8c979..4ea469a0b1c 100644 --- a/apps/web/src/app/tools/send/send-access/access.component.ts +++ b/apps/web/src/app/tools/send/send-access/access.component.ts @@ -1,161 +1,60 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; -import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; -import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; -import { SendAccessFileComponent } from "./send-access-file.component"; -import { SendAccessPasswordComponent } from "./send-access-password.component"; -import { SendAccessTextComponent } from "./send-access-text.component"; +import { SendAuthComponent } from "./send-auth.component"; +import { SendViewComponent } from "./send-view.component"; + +const SendViewState = Object.freeze({ + View: "view", + Auth: "auth", +} as const); +type SendViewState = (typeof SendViewState)[keyof typeof SendViewState]; // 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-send-access", templateUrl: "access.component.html", - imports: [ - SendAccessFileComponent, - SendAccessTextComponent, - SendAccessPasswordComponent, - SharedModule, - ], + imports: [SendAuthComponent, SendViewComponent, SharedModule], }) export class AccessComponent implements OnInit { - protected send: SendAccessView; - protected sendType = SendType; - protected loading = true; - protected passwordRequired = false; - protected formPromise: Promise; - protected password: string; - protected unavailable = false; - protected error = false; - protected hideEmail = false; - protected decKey: SymmetricCryptoKey; - protected accessRequest: SendAccessRequest; + viewState: SendViewState = SendViewState.View; + id: string; + key: string; - protected formGroup = this.formBuilder.group({}); + sendAccessResponse: SendAccessResponse | null = null; + sendAccessRequest: SendAccessRequest = new SendAccessRequest(); - private id: string; - private key: string; - - constructor( - private cryptoFunctionService: CryptoFunctionService, - private route: ActivatedRoute, - private keyService: KeyService, - private sendApiService: SendApiService, - private toastService: ToastService, - private i18nService: I18nService, - private layoutWrapperDataService: AnonLayoutWrapperDataService, - protected formBuilder: FormBuilder, - ) {} - - protected get expirationDate() { - if (this.send == null || this.send.expirationDate == null) { - return null; - } - return this.send.expirationDate; - } - - protected get creatorIdentifier() { - if (this.send == null || this.send.creatorIdentifier == null) { - return null; - } - return this.send.creatorIdentifier; - } + constructor(private route: ActivatedRoute) {} async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.params.subscribe(async (params) => { this.id = params.sendId; this.key = params.key; - if (this.key == null || this.id == null) { - return; + + if (this.id && this.key) { + this.viewState = SendViewState.View; + this.sendAccessResponse = null; + this.sendAccessRequest = new SendAccessRequest(); } - await this.load(); }); } - protected load = async () => { - this.unavailable = false; - this.error = false; - this.hideEmail = false; - try { - const keyArray = Utils.fromUrlB64ToArray(this.key); - this.accessRequest = new SendAccessRequest(); - if (this.password != null) { - const passwordHash = await this.cryptoFunctionService.pbkdf2( - this.password, - keyArray, - "sha256", - SEND_KDF_ITERATIONS, - ); - this.accessRequest.password = Utils.fromBufferToB64(passwordHash); - } - let sendResponse: SendAccessResponse = null; - if (this.loading) { - sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest); - } else { - this.formPromise = this.sendApiService.postSendAccess(this.id, this.accessRequest); - sendResponse = await this.formPromise; - } - this.passwordRequired = false; - const sendAccess = new SendAccess(sendResponse); - this.decKey = await this.keyService.makeSendKey(keyArray); - this.send = await sendAccess.decrypt(this.decKey); - } catch (e) { - if (e instanceof ErrorResponse) { - if (e.statusCode === 401) { - this.passwordRequired = true; - } else if (e.statusCode === 404) { - this.unavailable = true; - } else if (e.statusCode === 400) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: e.message, - }); - } else { - this.error = true; - } - } else { - this.error = true; - } - } - this.loading = false; - this.hideEmail = - this.creatorIdentifier == null && - !this.passwordRequired && - !this.loading && - !this.unavailable; + onAuthRequired() { + this.viewState = SendViewState.Auth; + } - if (this.creatorIdentifier != null) { - this.layoutWrapperDataService.setAnonLayoutWrapperData({ - pageSubtitle: { - key: "sendAccessCreatorIdentifier", - placeholders: [this.creatorIdentifier], - }, - }); - } - }; - - protected setPassword(password: string) { - this.password = password; + onAccessGranted(event: { response: SendAccessResponse; request: SendAccessRequest }) { + this.sendAccessResponse = event.response; + this.sendAccessRequest = event.request; + this.viewState = SendViewState.View; } } diff --git a/apps/web/src/app/tools/send/send-access/authentication-flow.md b/apps/web/src/app/tools/send/send-access/authentication-flow.md deleted file mode 100644 index f39b43fcd41..00000000000 --- a/apps/web/src/app/tools/send/send-access/authentication-flow.md +++ /dev/null @@ -1,75 +0,0 @@ -# Send Authentication Flows - -In the below diagrams, activations represent client control flow. - -## Public Sends - -Anyone can access a public send. The token endpoint automatically issues a token. It never issues a challenge. - -```mermaid -sequenceDiagram - participant Visitor - participant TryAccess as try-send-access.guard - participant SendToken as send-token API - participant ViewContent as view-content.component - participant SendAccess as send-access API - - Visitor->>TryAccess: Navigate to send URL - activate TryAccess - TryAccess->>SendToken: Request anonymous access token - SendToken-->>TryAccess: OK + Security token - TryAccess->>ViewContent: Redirect with token - deactivate TryAccess - activate ViewContent - ViewContent->>SendAccess: Request send content (with token and key) - SendAccess-->>ViewContent: Return send content - ViewContent->>Visitor: Display send content - deactivate ViewContent -``` - -## Password Protected Sends - -Password protected sends redirect to a password challenge prompt. - -```mermaid -sequenceDiagram - participant Visitor - participant TryAccess as try-send-access.guard - participant PasswordAuth as password-authentication.component - participant SendToken as send-token API - participant ViewContent as view-content.component - participant SendAccess as send-access API - - Visitor->>TryAccess: Navigate to send URL - activate TryAccess - TryAccess->>SendToken: Request anonymous access token - SendToken-->>TryAccess: Unauthorized + Password challenge - TryAccess->>PasswordAuth: Redirect with send ID and key - deactivate TryAccess - activate PasswordAuth - PasswordAuth->>Visitor: Request password - Visitor-->>PasswordAuth: Enter password - PasswordAuth->>SendToken: Request access token (with password) - SendToken-->>PasswordAuth: OK + Security token - deactivate PasswordAuth - activate ViewContent - PasswordAuth->>ViewContent: Redirect with token and send key - ViewContent->>SendAccess: Request send content (with token) - SendAccess-->>ViewContent: Return send content - ViewContent->>Visitor: Display send content - deactivate ViewContent -``` - -## Send Access without token - -Visiting the view page without a token redirects to a try-access flow, above. - -```mermaid -sequenceDiagram - participant Visitor - participant ViewContent as view-content.component - participant TryAccess as try-send-access.guard - - Visitor->>ViewContent: Navigate to send URL (with id and key) - ViewContent->>TryAccess: Redirect to try-access (with id and key) -``` diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts deleted file mode 100644 index cd07d3684fb..00000000000 --- a/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { TestBed, fakeAsync, tick } from "@angular/core/testing"; -import { Router, UrlTree } from "@angular/router"; -import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom, NEVER } from "rxjs"; - -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { mockAccountServiceWith, FakeStateProvider } from "@bitwarden/common/spec"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { UserId } from "@bitwarden/common/types/guid"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; - -import { DefaultSendAccessService } from "./default-send-access-service"; -import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; - -describe("DefaultSendAccessService", () => { - let service: DefaultSendAccessService; - let stateProvider: FakeStateProvider; - let sendApiService: MockProxy; - let router: MockProxy; - let logger: MockProxy; - let systemServiceProvider: MockProxy; - - beforeEach(() => { - const accountService = mockAccountServiceWith("user-id" as UserId); - stateProvider = new FakeStateProvider(accountService); - sendApiService = mock(); - router = mock(); - logger = mock(); - systemServiceProvider = mock(); - - systemServiceProvider.log.mockReturnValue(logger); - - TestBed.configureTestingModule({ - providers: [ - DefaultSendAccessService, - { provide: StateProvider, useValue: stateProvider }, - { provide: SendApiService, useValue: sendApiService }, - { provide: Router, useValue: router }, - { provide: SYSTEM_SERVICE_PROVIDER, useValue: systemServiceProvider }, - ], - }); - - service = TestBed.inject(DefaultSendAccessService); - }); - - describe("constructor", () => { - it("creates logger with type 'SendAccessAuthenticationService' when initialized", () => { - expect(systemServiceProvider.log).toHaveBeenCalledWith({ - type: "SendAccessAuthenticationService", - }); - }); - }); - - describe("redirect$", () => { - const sendId = "test-send-id"; - - it("returns content page UrlTree and logs info when API returns success", async () => { - const expectedUrlTree = { toString: () => "/send/content/test-send-id" } as UrlTree; - sendApiService.postSendAccess.mockResolvedValue({} as any); - router.createUrlTree.mockReturnValue(expectedUrlTree); - - const result = await firstValueFrom(service.redirect$(sendId)); - - expect(result).toBe(expectedUrlTree); - expect(logger.info).toHaveBeenCalledWith( - "public send detected; redirecting to send access with token.", - ); - }); - - describe("given error responses", () => { - it("returns password flow UrlTree and logs debug when 401 received", async () => { - const expectedUrlTree = { toString: () => "/send/test-send-id" } as UrlTree; - const errorResponse = new ErrorResponse([], 401); - sendApiService.postSendAccess.mockRejectedValue(errorResponse); - router.createUrlTree.mockReturnValue(expectedUrlTree); - - const result = await firstValueFrom(service.redirect$(sendId)); - - expect(result).toBe(expectedUrlTree); - expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to password flow"); - }); - - it("returns 404 page UrlTree and logs debug when 404 received", async () => { - const expectedUrlTree = { toString: () => "/404.html" } as UrlTree; - const errorResponse = new ErrorResponse([], 404); - sendApiService.postSendAccess.mockRejectedValue(errorResponse); - router.parseUrl.mockReturnValue(expectedUrlTree); - - const result = await firstValueFrom(service.redirect$(sendId)); - - expect(result).toBe(expectedUrlTree); - expect(logger.debug).toHaveBeenCalledWith(errorResponse, "redirecting to unavailable page"); - }); - - it("logs warning and throws error when 500 received", async () => { - const errorResponse = new ErrorResponse([], 500); - sendApiService.postSendAccess.mockRejectedValue(errorResponse); - - await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse); - expect(logger.warn).toHaveBeenCalledWith( - errorResponse, - "received unexpected error response", - ); - }); - - it("throws error when unexpected error code received", async () => { - const errorResponse = new ErrorResponse([], 403); - sendApiService.postSendAccess.mockRejectedValue(errorResponse); - - await expect(firstValueFrom(service.redirect$(sendId))).rejects.toBe(errorResponse); - expect(logger.warn).toHaveBeenCalledWith( - errorResponse, - "received unexpected error response", - ); - }); - }); - - it("throws error when non-ErrorResponse error occurs", async () => { - const regularError = new Error("Network error"); - sendApiService.postSendAccess.mockRejectedValue(regularError); - - await expect(firstValueFrom(service.redirect$(sendId))).rejects.toThrow("Network error"); - expect(logger.warn).not.toHaveBeenCalled(); - }); - - it("emits timeout error when API response exceeds 10 seconds", fakeAsync(() => { - // Mock API to never resolve (simulating a hung request) - sendApiService.postSendAccess.mockReturnValue(firstValueFrom(NEVER)); - - const result$ = service.redirect$(sendId); - let error: any; - - result$.subscribe({ - error: (err: unknown) => (error = err), - }); - - // Advance time past 10 seconds - tick(10001); - - expect(error).toBeDefined(); - expect(error.name).toBe("TimeoutError"); - })); - }); - - describe("setContext", () => { - it("updates global state with send context when called with sendId and key", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - - await service.setContext(sendId, key); - - const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$); - expect(context).toEqual({ id: sendId, key }); - }); - }); - - describe("clear", () => { - it("sets both SEND_RESPONSE_KEY and SEND_CONTEXT_KEY to null when called", async () => { - // Set initial values - await stateProvider.getGlobal(SEND_RESPONSE_KEY).update(() => ({ some: "response" }) as any); - await stateProvider.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: "test", key: "test" })); - - await service.clear(); - - const response = await firstValueFrom(stateProvider.getGlobal(SEND_RESPONSE_KEY).state$); - const context = await firstValueFrom(stateProvider.getGlobal(SEND_CONTEXT_KEY).state$); - - expect(response).toBeNull(); - expect(context).toBeNull(); - }); - }); -}); diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.ts deleted file mode 100644 index 732303ce25a..00000000000 --- a/apps/web/src/app/tools/send/send-access/default-send-access-service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Injectable, Inject } from "@angular/core"; -import { Router, UrlTree } from "@angular/router"; -import { map, of, from, catchError, timeout } from "rxjs"; - -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; -import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; - -import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; -import { SendAccessService } from "./send-access-service.abstraction"; -import { isErrorResponse } from "./util"; - -const TEN_SECONDS = 10_000; - -@Injectable({ providedIn: "root" }) -export class DefaultSendAccessService implements SendAccessService { - private readonly logger: SemanticLogger; - - constructor( - private readonly state: StateProvider, - private readonly api: SendApiService, - private readonly router: Router, - @Inject(SYSTEM_SERVICE_PROVIDER) system: SystemServiceProvider, - ) { - this.logger = system.log({ type: "SendAccessAuthenticationService" }); - } - - redirect$(sendId: string) { - // FIXME: when the send authentication APIs become available, this method - // should delegate to the API - const response$ = from(this.api.postSendAccess(sendId, new SendAccessRequest())); - - const redirect$ = response$.pipe( - timeout({ first: TEN_SECONDS }), - map((_response) => { - this.logger.info("public send detected; redirecting to send access with token."); - const url = this.toViewRedirect(sendId); - - return url; - }), - catchError((error: unknown) => { - let processed: UrlTree | undefined = undefined; - - if (isErrorResponse(error)) { - processed = this.toErrorRedirect(sendId, error); - } - - if (processed) { - return of(processed); - } - - throw error; - }), - ); - - return redirect$; - } - - private toViewRedirect(sendId: string) { - return this.router.createUrlTree(["send", "content", sendId]); - } - - private toErrorRedirect(sendId: string, response: ErrorResponse) { - let url: UrlTree | undefined = undefined; - - switch (response.statusCode) { - case 401: - this.logger.debug(response, "redirecting to password flow"); - url = this.router.createUrlTree(["send/password", sendId]); - break; - - case 404: - this.logger.debug(response, "redirecting to unavailable page"); - url = this.router.parseUrl("/404.html"); - break; - - default: - this.logger.warn(response, "received unexpected error response"); - } - - return url; - } - - async setContext(sendId: string, key: string) { - await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => ({ id: sendId, key })); - } - - async clear(): Promise { - await this.state.getGlobal(SEND_RESPONSE_KEY).update(() => null); - await this.state.getGlobal(SEND_CONTEXT_KEY).update(() => null); - } -} diff --git a/apps/web/src/app/tools/send/send-access/index.ts b/apps/web/src/app/tools/send/send-access/index.ts index 4bef65f468b..c9df5ce5193 100644 --- a/apps/web/src/app/tools/send/send-access/index.ts +++ b/apps/web/src/app/tools/send/send-access/index.ts @@ -1,4 +1,2 @@ export { AccessComponent } from "./access.component"; export { SendAccessExplainerComponent } from "./send-access-explainer.component"; - -export { SendAccessRoutes } from "./routes"; diff --git a/apps/web/src/app/tools/send/send-access/routes.ts b/apps/web/src/app/tools/send/send-access/routes.ts deleted file mode 100644 index e94453c9351..00000000000 --- a/apps/web/src/app/tools/send/send-access/routes.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Routes } from "@angular/router"; - -import { ActiveSendIcon } from "@bitwarden/assets/svg"; -import { AnonLayoutWrapperData } from "@bitwarden/components"; - -import { RouteDataProperties } from "../../../core"; - -import { SendAccessExplainerComponent } from "./send-access-explainer.component"; -import { SendAccessPasswordComponent } from "./send-access-password.component"; -import { trySendAccess } from "./try-send-access.guard"; - -/** Routes to reach send access screens */ -export const SendAccessRoutes: Routes = [ - { - path: "send/:sendId", - // there are no child pages because `trySendAccess` always performs a redirect - canActivate: [trySendAccess], - }, - { - path: "send/password/:sendId", - data: { - pageTitle: { - key: "sendAccessPasswordTitle", - }, - pageIcon: ActiveSendIcon, - showReadonlyHostname: true, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: SendAccessPasswordComponent, - }, - { - path: "", - outlet: "secondary", - component: SendAccessExplainerComponent, - }, - ], - }, - { - path: "send/content/:sendId", - data: { - pageTitle: { - key: "sendAccessContentTitle", - }, - pageIcon: ActiveSendIcon, - showReadonlyHostname: true, - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "send/password/:sendId", - }, - { - path: "", - outlet: "secondary", - component: SendAccessExplainerComponent, - }, - ], - }, -]; diff --git a/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts b/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts deleted file mode 100644 index 8d7fe9cd380..00000000000 --- a/apps/web/src/app/tools/send/send-access/send-access-memory.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { KeyDefinition, SEND_ACCESS_AUTH_MEMORY } from "@bitwarden/common/platform/state"; -import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; - -import { SEND_CONTEXT_KEY, SEND_RESPONSE_KEY } from "./send-access-memory"; -import { SendContext } from "./types"; - -describe("send-access-memory", () => { - describe("SEND_CONTEXT_KEY", () => { - it("has correct state definition properties", () => { - expect(SEND_CONTEXT_KEY).toBeInstanceOf(KeyDefinition); - expect(SEND_CONTEXT_KEY.stateDefinition).toBe(SEND_ACCESS_AUTH_MEMORY); - expect(SEND_CONTEXT_KEY.key).toBe("sendContext"); - }); - - it("deserializes data as-is", () => { - const testContext: SendContext = { id: "test-id", key: "test-key" }; - const deserializer = SEND_CONTEXT_KEY.deserializer; - expect(deserializer(testContext)).toBe(testContext); - }); - - it("deserializes null as null", () => { - const deserializer = SEND_CONTEXT_KEY.deserializer; - expect(deserializer(null)).toBe(null); - }); - }); - - describe("SEND_RESPONSE_KEY", () => { - it("has correct state definition properties", () => { - expect(SEND_RESPONSE_KEY).toBeInstanceOf(KeyDefinition); - expect(SEND_RESPONSE_KEY.stateDefinition).toBe(SEND_ACCESS_AUTH_MEMORY); - expect(SEND_RESPONSE_KEY.key).toBe("sendResponse"); - }); - - it("deserializes data into SendAccessResponse instance", () => { - const mockData = { id: "test-id", name: "test-send" } as any; - const deserializer = SEND_RESPONSE_KEY.deserializer; - const result = deserializer(mockData); - - expect(result).toBeInstanceOf(SendAccessResponse); - }); - - it.each([ - [null, "null"], - [undefined, "undefined"], - ])("deserializes %s as null", (value, _) => { - const deserializer = SEND_RESPONSE_KEY.deserializer; - expect(deserializer(value!)).toBe(null); - }); - }); -}); diff --git a/apps/web/src/app/tools/send/send-access/send-access-memory.ts b/apps/web/src/app/tools/send/send-access/send-access-memory.ts deleted file mode 100644 index 4f67cf43b37..00000000000 --- a/apps/web/src/app/tools/send/send-access/send-access-memory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { KeyDefinition, SEND_ACCESS_AUTH_MEMORY } from "@bitwarden/common/platform/state"; -import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; - -import { SendContext } from "./types"; - -export const SEND_CONTEXT_KEY = new KeyDefinition( - SEND_ACCESS_AUTH_MEMORY, - "sendContext", - { - deserializer: (data) => data, - }, -); - -/** When send authentication succeeds, this stores the result so that - * multiple access attempts don't accrue due to the send workflow. - */ -// FIXME: replace this with the send authentication token once it's -// available -export const SEND_RESPONSE_KEY = new KeyDefinition( - SEND_ACCESS_AUTH_MEMORY, - "sendResponse", - { - deserializer: (data) => (data ? new SendAccessResponse(data) : null), - }, -); diff --git a/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts b/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts deleted file mode 100644 index 66fc87fe802..00000000000 --- a/apps/web/src/app/tools/send/send-access/send-access-service.abstraction.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UrlTree } from "@angular/router"; -import { Observable } from "rxjs"; - -export abstract class SendAccessService { - abstract redirect$: (sendId: string) => Observable; - - abstract setContext: (sendId: string, key: string) => Promise; - - abstract clear: () => Promise; -} 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 new file mode 100644 index 00000000000..21a6de50ba8 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.html @@ -0,0 +1,14 @@ +
+
+

{{ "sendAccessUnavailable" | i18n }}

+
+
+

{{ "unexpectedErrorSend" | i18n }}

+
+ + +
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 new file mode 100644 index 00000000000..b360044a8b6 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-auth.component.ts @@ -0,0 +1,86 @@ +import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; + +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.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 { ToastService } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +import { SendAccessPasswordComponent } from "./send-access-password.component"; + +@Component({ + selector: "app-send-auth", + templateUrl: "send-auth.component.html", + imports: [SendAccessPasswordComponent, SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendAuthComponent { + readonly id = input.required(); + readonly key = input.required(); + + accessGranted = output<{ + response: SendAccessResponse; + request: SendAccessRequest; + }>(); + + loading = false; + error = false; + unavailable = false; + password?: string; + + private accessRequest!: SendAccessRequest; + + constructor( + private cryptoFunctionService: CryptoFunctionService, + private sendApiService: SendApiService, + private toastService: ToastService, + private i18nService: I18nService, + ) {} + + async onSubmit(password: string) { + this.password = password; + this.loading = true; + this.error = false; + this.unavailable = false; + + try { + const keyArray = Utils.fromUrlB64ToArray(this.key()); + this.accessRequest = new SendAccessRequest(); + + const passwordHash = await this.cryptoFunctionService.pbkdf2( + this.password, + keyArray, + "sha256", + SEND_KDF_ITERATIONS, + ); + this.accessRequest.password = Utils.fromBufferToB64(passwordHash); + + const sendResponse = await this.sendApiService.postSendAccess(this.id(), this.accessRequest); + this.accessGranted.emit({ response: sendResponse, request: this.accessRequest }); + } catch (e) { + if (e instanceof ErrorResponse) { + if (e.statusCode === 404) { + this.unavailable = true; + } else if (e.statusCode === 400) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); + } else { + this.error = true; + } + } else { + this.error = true; + } + } finally { + this.loading = false; + } + } +} 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 new file mode 100644 index 00000000000..dd0b770b261 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-view.component.html @@ -0,0 +1,47 @@ + + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ + "learnMore" | i18n + }}. + + + +
+

{{ "sendAccessUnavailable" | i18n }}

+
+
+

{{ "unexpectedErrorSend" | i18n }}

+
+
+

+ {{ send.name }} +

+
+ + + + + + + + +

+ Expires: {{ expirationDate | date: "medium" }} +

+
+
+ +
+ + {{ "loading" | i18n }} +
+
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 new file mode 100644 index 00000000000..060dc1958b1 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access/send-view.component.ts @@ -0,0 +1,131 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + input, + OnInit, + output, +} from "@angular/core"; + +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.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 { KeyService } from "@bitwarden/key-management"; + +import { SharedModule } from "../../../shared"; + +import { SendAccessFileComponent } from "./send-access-file.component"; +import { SendAccessTextComponent } from "./send-access-text.component"; + +@Component({ + selector: "app-send-view", + templateUrl: "send-view.component.html", + imports: [SendAccessFileComponent, SendAccessTextComponent, SharedModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendViewComponent implements OnInit { + readonly id = input.required(); + readonly key = input.required(); + readonly sendResponse = input(null); + readonly accessRequest = input(new SendAccessRequest()); + + authRequired = output(); + + send: SendAccessView | null = null; + sendType = SendType; + loading = true; + unavailable = false; + error = false; + hideEmail = false; + decKey!: SymmetricCryptoKey; + + constructor( + private keyService: KeyService, + private sendApiService: SendApiService, + private toastService: ToastService, + private i18nService: I18nService, + private layoutWrapperDataService: AnonLayoutWrapperDataService, + private cdRef: ChangeDetectorRef, + ) {} + + get expirationDate() { + if (this.send == null || this.send.expirationDate == null) { + return null; + } + return this.send.expirationDate; + } + + get creatorIdentifier() { + if (this.send == null || this.send.creatorIdentifier == null) { + return null; + } + return this.send.creatorIdentifier; + } + + async ngOnInit() { + await this.load(); + } + + private async load() { + this.unavailable = false; + this.error = false; + this.hideEmail = false; + this.loading = true; + + let response = this.sendResponse(); + + try { + if (!response) { + response = await this.sendApiService.postSendAccess(this.id(), this.accessRequest()); + } + + const keyArray = Utils.fromUrlB64ToArray(this.key()); + const sendAccess = new SendAccess(response); + this.decKey = await this.keyService.makeSendKey(keyArray); + this.send = await sendAccess.decrypt(this.decKey); + } catch (e) { + if (e instanceof ErrorResponse) { + if (e.statusCode === 401) { + this.authRequired.emit(); + } else if (e.statusCode === 404) { + this.unavailable = true; + } else if (e.statusCode === 400) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: e.message, + }); + } else { + this.error = true; + } + } else { + this.error = true; + } + } + + this.loading = false; + this.hideEmail = + this.creatorIdentifier == null && !this.loading && !this.unavailable && !response; + + this.hideEmail = this.send != null && this.creatorIdentifier == null; + + if (this.creatorIdentifier != null) { + this.layoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + key: "sendAccessCreatorIdentifier", + placeholders: [this.creatorIdentifier], + }, + }); + } + + this.cdRef.markForCheck(); + } +} diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts deleted file mode 100644 index 267de83db9f..00000000000 --- a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { TestBed } from "@angular/core/testing"; -import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from "@angular/router"; -import { firstValueFrom, Observable, of } from "rxjs"; - -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; - -import { SendAccessService } from "./send-access-service.abstraction"; -import { trySendAccess } from "./try-send-access.guard"; - -function createMockRoute(params: Record): ActivatedRouteSnapshot { - return { params } as ActivatedRouteSnapshot; -} - -function createMockLogger(): SemanticLogger { - return { - warn: jest.fn(), - panic: jest.fn().mockImplementation(() => { - throw new Error("Logger panic called"); - }), - } as any as SemanticLogger; -} - -function createMockSystemServiceProvider(): SystemServiceProvider { - return { - log: jest.fn().mockReturnValue(createMockLogger()), - } as any as SystemServiceProvider; -} - -function createMockSendAccessService() { - return { - setContext: jest.fn().mockResolvedValue(undefined), - redirect$: jest.fn().mockReturnValue(of({} as UrlTree)), - clear: jest.fn().mockResolvedValue(undefined), - }; -} - -describe("trySendAccess", () => { - let mockSendAccessService: ReturnType; - let mockSystemServiceProvider: SystemServiceProvider; - let mockRouterState: RouterStateSnapshot; - - beforeEach(() => { - mockSendAccessService = createMockSendAccessService(); - mockSystemServiceProvider = createMockSystemServiceProvider(); - mockRouterState = {} as RouterStateSnapshot; - - TestBed.configureTestingModule({ - providers: [ - { provide: SendAccessService, useValue: mockSendAccessService }, - { provide: SYSTEM_SERVICE_PROVIDER, useValue: mockSystemServiceProvider }, - ], - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("canActivate", () => { - describe("given valid route parameters", () => { - it("extracts sendId and key from route params when both are valid strings", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - // need to cast the result because `CanActivateFn` performs type erasure - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); - expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1); - await expect(firstValueFrom(result$)).resolves.toEqual(expectedUrlTree); - }); - - it("does not throw validation errors when sendId and key are valid strings", async () => { - const sendId = "valid-send-id"; - const key = "valid-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - // Should not throw any errors during guard execution - let guardResult: Observable | undefined; - expect(() => { - guardResult = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - }).not.toThrow(); - - // Verify the observable can be subscribed to without errors - expect(guardResult).toBeDefined(); - await expect(firstValueFrom(guardResult!)).resolves.toEqual(expectedUrlTree); - - // Logger methods should not be called for warnings or panics - const mockLogger = (mockSystemServiceProvider.log as jest.Mock).mock.results[0].value; - expect(mockLogger.warn).not.toHaveBeenCalled(); - expect(mockLogger.panic).not.toHaveBeenCalled(); - }); - }); - - describe("given invalid route parameters", () => { - describe("given invalid sendId", () => { - it.each([ - ["undefined", undefined], - ["null", null], - ])( - "logs warning with correct message when sendId is %s", - async (description, sendIdValue) => { - const key = "valid-key"; - const mockRoute = createMockRoute( - sendIdValue === undefined ? { key } : { sendId: sendIdValue, key }, - ); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ - function: "trySendAccess", - }); - expect(mockLogger.warn).toHaveBeenCalledWith( - "sendId missing from the route parameters; redirecting to 404", - ); - }, - ); - - it.each([ - ["number", 123], - ["object", {}], - ["boolean", true], - ])("logs panic with expected/actual type info when sendId is %s", async (type, value) => { - const key = "valid-key"; - const mockRoute = createMockRoute({ sendId: value, key }); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); - expect(mockLogger.panic).toHaveBeenCalledWith( - { expected: "string", actual: type }, - "sendId has invalid type", - ); - }); - - it("throws when sendId is not a string", async () => { - const key = "valid-key"; - const invalidSendIdValues = [123, {}, true, null, undefined]; - - for (const invalidSendId of invalidSendIdValues) { - const mockRoute = createMockRoute( - invalidSendId === undefined ? { key } : { sendId: invalidSendId, key }, - ); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - } - }); - }); - - describe("given invalid key", () => { - it.each([ - ["undefined", undefined], - ["null", null], - ])("logs panic with correct message when key is %s", async (description, keyValue) => { - const sendId = "valid-send-id"; - const mockRoute = createMockRoute( - keyValue === undefined ? { sendId } : { sendId, key: keyValue }, - ); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); - expect(mockLogger.panic).toHaveBeenCalledWith("key missing from the route parameters"); - }); - - it.each([ - ["number", 123], - ["object", {}], - ["boolean", true], - ])("logs panic with expected/actual type info when key is %s", async (type, value) => { - const sendId = "valid-send-id"; - const mockRoute = createMockRoute({ sendId, key: value }); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); - expect(mockLogger.panic).toHaveBeenCalledWith( - { expected: "string", actual: type }, - "key has invalid type", - ); - }); - - it("throws when key is not a string", async () => { - const sendId = "valid-send-id"; - const invalidKeyValues = [123, {}, true, null, undefined]; - - for (const invalidKey of invalidKeyValues) { - const mockRoute = createMockRoute( - invalidKey === undefined ? { sendId } : { sendId, key: invalidKey }, - ); - const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); - - await expect(async () => { - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - await firstValueFrom(result$); - }).rejects.toThrow("Logger panic called"); - } - }); - }); - }); - - describe("given service interactions", () => { - it("calls setContext with extracted sendId and key when parameters are valid", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - await firstValueFrom(result$); - - expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); - expect(mockSendAccessService.setContext).toHaveBeenCalledTimes(1); - }); - - it("calls redirect$ with extracted sendId when setContext completes", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - await firstValueFrom(result$); - - expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); - expect(mockSendAccessService.redirect$).toHaveBeenCalledTimes(1); - }); - }); - - describe("given observable behavior", () => { - it("returns redirect$ emissions when setContext completes successfully", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - const actualResult = await firstValueFrom(result$); - - expect(actualResult).toEqual(expectedUrlTree); - expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); - }); - - it("does not emit setContext values when using ignoreElements", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - const setContextValue = "should-not-be-emitted"; - - // Mock setContext to return a value - mockSendAccessService.setContext.mockResolvedValue(setContextValue); - mockSendAccessService.redirect$.mockReturnValue(of(expectedUrlTree)); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - const actualResult = await firstValueFrom(result$); - - // Should only emit the redirect$ value, not the setContext value - expect(actualResult).toEqual(expectedUrlTree); - expect(actualResult).not.toEqual(setContextValue); - }); - - it("ensures setContext completes before redirect$ executes (sequencing)", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const expectedUrlTree = { toString: () => "/test-url" } as UrlTree; - - let setContextResolved = false; - - // Mock setContext to track when it resolves - mockSendAccessService.setContext.mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)); // Small delay - setContextResolved = true; - }); - - // Mock redirect$ to return a delayed observable and check if setContext resolved - mockSendAccessService.redirect$.mockImplementation((id) => { - return new Observable((subscriber) => { - // Check if setContext has resolved when redirect$ subscription starts - setTimeout(() => { - expect(setContextResolved).toBe(true); - subscriber.next(expectedUrlTree); - subscriber.complete(); - }, 0); - }); - }); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - await firstValueFrom(result$); - }); - }); - - describe("given error scenarios", () => { - it("does not call redirect$ when setContext rejects", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const setContextError = new Error("setContext failed"); - - // Reset mocks to ensure clean state - jest.clearAllMocks(); - - // Mock setContext to reject - mockSendAccessService.setContext.mockRejectedValue(setContextError); - - // Create a mock observable that we can spy on subscription - const mockRedirectObservable = of({} as UrlTree); - const subscribeSpy = jest.spyOn(mockRedirectObservable, "subscribe"); - mockSendAccessService.redirect$.mockReturnValue(mockRedirectObservable); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - // Expect the observable to reject when setContext fails - await expect(firstValueFrom(result$)).rejects.toThrow("setContext failed"); - - // The redirect$ method will be called (since it's called synchronously) - expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); - - // But the returned observable should not be subscribed to due to the error - // Note: This test verifies the error propagation behavior - expect(subscribeSpy).not.toHaveBeenCalled(); - }); - - it("propagates error to guard return value when redirect$ throws", async () => { - const sendId = "test-send-id"; - const key = "test-key"; - const mockRoute = createMockRoute({ sendId, key }); - const redirectError = new Error("redirect$ failed"); - - // Reset mocks to ensure clean state - jest.clearAllMocks(); - - // Mock setContext to succeed and redirect$ to throw - mockSendAccessService.setContext.mockResolvedValue(undefined); - mockSendAccessService.redirect$.mockReturnValue( - new Observable((subscriber) => { - subscriber.error(redirectError); - }), - ); - - const result$ = TestBed.runInInjectionContext(() => - trySendAccess(mockRoute, mockRouterState), - ) as unknown as Observable; - - // Expect the observable to propagate the redirect$ error - await expect(firstValueFrom(result$)).rejects.toThrow("redirect$ failed"); - - // Verify that setContext was called (should succeed) - expect(mockSendAccessService.setContext).toHaveBeenCalledWith(sendId, key); - - // Verify that redirect$ was called (but it throws) - expect(mockSendAccessService.redirect$).toHaveBeenCalledWith(sendId); - }); - }); - }); -}); diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts deleted file mode 100644 index 51941bf8e74..00000000000 --- a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; -import { from, ignoreElements, concat } from "rxjs"; - -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; - -import { SendAccessService } from "./send-access-service.abstraction"; - -export const trySendAccess: CanActivateFn = ( - route: ActivatedRouteSnapshot, - _state: RouterStateSnapshot, -) => { - const sendAccess = inject(SendAccessService); - const system = inject(SYSTEM_SERVICE_PROVIDER); - const logger = system.log({ function: "trySendAccess" }); - - const { sendId, key } = route.params; - if (!sendId) { - logger.warn("sendId missing from the route parameters; redirecting to 404"); - } - if (typeof sendId !== "string") { - logger.panic({ expected: "string", actual: typeof sendId }, "sendId has invalid type"); - } - - if (!key) { - logger.panic("key missing from the route parameters"); - } - if (typeof key !== "string") { - logger.panic({ expected: "string", actual: typeof key }, "key has invalid type"); - } - - const contextUpdated$ = from(sendAccess.setContext(sendId, key)).pipe(ignoreElements()); - const redirect$ = sendAccess.redirect$(sendId); - - // ensure the key has loaded before redirecting - return concat(contextUpdated$, redirect$); -}; diff --git a/apps/web/src/app/tools/send/send-access/types.ts b/apps/web/src/app/tools/send/send-access/types.ts deleted file mode 100644 index 03e058ca681..00000000000 --- a/apps/web/src/app/tools/send/send-access/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** global contextual information for the current send access page. */ -export type SendContext = { - /** identifies the send */ - id: string; - - /** decrypts the send content */ - key: string; -}; diff --git a/apps/web/src/app/tools/send/send-access/util.spec.ts b/apps/web/src/app/tools/send/send-access/util.spec.ts deleted file mode 100644 index 45502ee2509..00000000000 --- a/apps/web/src/app/tools/send/send-access/util.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; - -import { isErrorResponse, isSendContext } from "./util"; - -describe("util", () => { - describe("isErrorResponse", () => { - it("returns true when value is an ErrorResponse instance", () => { - const error = new ErrorResponse(["Error message"], 400); - expect(isErrorResponse(error)).toBe(true); - }); - - it.each([ - [null, "null"], - [undefined, "undefined"], - ])("returns false when value is %s", (value, description) => { - expect(isErrorResponse(value)).toBe(false); - }); - - it.each([ - ["string", "string"], - [123, "number"], - [true, "boolean"], - [{}, "plain object"], - [[], "array"], - ])("returns false when value is not an ErrorResponse (%s)", (value, description) => { - expect(isErrorResponse(value)).toBe(false); - }); - - it("returns false when value is a different Error type", () => { - const error = new Error("test"); - expect(isErrorResponse(error)).toBe(false); - }); - }); - - describe("isSendContext", () => { - it("returns true when value has id and key properties", () => { - const validContext = { id: "test-id", key: "test-key" }; - expect(isSendContext(validContext)).toBe(true); - }); - - it("returns true even with additional properties", () => { - const contextWithExtras = { id: "test-id", key: "test-key", extra: "data" }; - expect(isSendContext(contextWithExtras)).toBe(true); - }); - - it.each([ - [null, "null"], - [undefined, "undefined"], - ])("returns false when value is %s", (value, _) => { - expect(isSendContext(value)).toBe(false); - }); - - it.each([ - ["string", "string"], - [123, "number"], - [true, "boolean"], - ])("returns false when value is not an object (%s)", (value, _) => { - expect(isSendContext(value)).toBe(false); - }); - - it.each([ - [{ key: "test-key" }, "missing id"], - [{ id: "test-id" }, "missing key"], - [{}, "empty object"], - ])("returns false when value is %s", (value, _) => { - expect(isSendContext(value)).toBe(false); - }); - }); -}); diff --git a/apps/web/src/app/tools/send/send-access/util.ts b/apps/web/src/app/tools/send/send-access/util.ts deleted file mode 100644 index d9cbef0d337..00000000000 --- a/apps/web/src/app/tools/send/send-access/util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; - -import { SendContext } from "./types"; - -/** narrows a type to an `ErrorResponse` */ -export function isErrorResponse(value: unknown): value is ErrorResponse { - return value instanceof ErrorResponse; -} - -/** narrows a type to a `SendContext` */ -export function isSendContext(value: unknown): value is SendContext { - return !!value && typeof value === "object" && "id" in value && "key" in value; -} diff --git a/apps/web/src/app/tools/send/send.component.html b/apps/web/src/app/tools/send/send.component.html index 6418744a727..a40cb3d4330 100644 --- a/apps/web/src/app/tools/send/send.component.html +++ b/apps/web/src/app/tools/send/send.component.html @@ -17,100 +17,159 @@ {{ "sendDisabledWarning" | i18n }} -
-
-
-
- {{ "filters" | i18n }} -
-
-
- -
-
-
    -
  • - - - -
  • -
-
-
-
-

{{ "types" | i18n }}

-
-
    -
  • - - - -
  • -
  • - - - -
  • -
-
-
-
-
-
- -
- - - {{ "loading" | i18n }} - - - - {{ "sendsTitleNoItems" | i18n }} - {{ "sendsBodyNoItems" | i18n }} - - - +@if (SendUIRefresh$ | async) { +
+ +
+ +
+ + {{ "allSends" | i18n }} + {{ "sendTypeText" | i18n }} + {{ "sendTypeFile" | i18n }} + +
+ +
+
+ + +
+ + + {{ "loading" | i18n }} + + + + {{ "sendsTitleNoItems" | i18n }} + {{ "sendsBodyNoItems" | i18n }} + + + +
-
+} @else { +
+
+
+
+ {{ "filters" | i18n }} +
+
+
+ +
+
+
    +
  • + + + +
  • +
+
+
+
+

{{ "types" | i18n }}

+
+
    +
  • + + + +
  • +
  • + + + +
  • +
+
+
+
+
+
+ + +
+ + + {{ "loading" | i18n }} + + + + {{ "sendsTitleNoItems" | i18n }} + {{ "sendsBodyNoItems" | i18n }} + + + +
+
+
+} diff --git a/apps/web/src/app/tools/send/send.component.ts b/apps/web/src/app/tools/send/send.component.ts index 7c0e03e3e21..db45b104900 100644 --- a/apps/web/src/app/tools/send/send.component.ts +++ b/apps/web/src/app/tools/send/send.component.ts @@ -1,7 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, NgZone, OnInit, OnDestroy } from "@angular/core"; -import { lastValueFrom } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { lastValueFrom, Observable, switchMap, EMPTY } from "rxjs"; import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; import { NoSendsIcon } from "@bitwarden/assets/svg"; @@ -17,6 +19,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SendFilterType } from "@bitwarden/common/tools/send/types/send-filter-type"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; import { SendId } from "@bitwarden/common/types/guid"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { @@ -26,6 +30,7 @@ import { SearchModule, TableDataSource, ToastService, + ToggleGroupModule, } from "@bitwarden/components"; import { DefaultSendFormConfigService, @@ -39,6 +44,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { NewSendDropdownComponent } from "./new-send/new-send-dropdown.component"; +import { SendSuccessDrawerDialogComponent } from "./shared"; const BroadcasterSubscriptionId = "SendComponent"; @@ -52,6 +58,7 @@ const BroadcasterSubscriptionId = "SendComponent"; NoItemsModule, HeaderModule, NewSendDropdownComponent, + ToggleGroupModule, SendTableComponent, ], templateUrl: "send.component.html", @@ -60,6 +67,8 @@ const BroadcasterSubscriptionId = "SendComponent"; export class SendComponent extends BaseSendComponent implements OnInit, OnDestroy { private sendItemDialogRef?: DialogRef | undefined; noItemIcon = NoSendsIcon; + selectedToggleValue?: SendFilterType; + SendUIRefresh$: Observable; override set filteredSends(filteredSends: SendView[]) { super.filteredSends = filteredSends; @@ -87,6 +96,8 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro toastService: ToastService, private addEditFormConfigService: DefaultSendFormConfigService, accountService: AccountService, + private route: ActivatedRoute, + private router: Router, private configService: ConfigService, ) { super( @@ -103,10 +114,38 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro toastService, accountService, ); + + this.SendUIRefresh$ = this.configService.getFeatureFlag$(FeatureFlag.SendUIRefresh); + + this.SendUIRefresh$.pipe( + switchMap((sendUiRefreshEnabled) => { + if (sendUiRefreshEnabled) { + return this.route.queryParamMap; + } + return EMPTY; + }), + takeUntilDestroyed(), + ).subscribe((params) => { + const typeParam = params.get("type"); + const value = ( + typeParam === SendFilterType.Text || typeParam === SendFilterType.File + ? typeParam + : SendFilterType.All + ) as SendFilterType; + this.selectedToggleValue = value; + + if (this.loaded) { + this.applyTypeFilter(value); + } + }); } async ngOnInit() { await super.ngOnInit(); + this.onSuccessfulLoad = async () => { + this.applyTypeFilter(this.selectedToggleValue); + }; + await this.load(); // Broadcaster subscription - load if sync completes in the background @@ -172,12 +211,49 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro }); } - const result = await lastValueFrom(this.sendItemDialogRef.closed); + const result: SendItemDialogResult = await lastValueFrom(this.sendItemDialogRef.closed); this.sendItemDialogRef = undefined; // If the dialog was closed by deleting the cipher, refresh the vault. - if (result === SendItemDialogResult.Deleted || result === SendItemDialogResult.Saved) { + if ( + result?.result === SendItemDialogResult.Deleted || + result?.result === SendItemDialogResult.Saved + ) { await this.load(); } + + if ( + result?.result === SendItemDialogResult.Saved && + result?.send && + (await this.configService.getFeatureFlag(FeatureFlag.SendUIRefresh)) + ) { + this.dialogService.openDrawer(SendSuccessDrawerDialogComponent, { + data: result.send, + }); + } + } + + private applyTypeFilter(value: SendFilterType) { + if (value === SendFilterType.All) { + this.selectAll(); + } else if (value === SendFilterType.Text) { + this.selectType(SendType.Text); + } else if (value === SendFilterType.File) { + this.selectType(SendType.File); + } + } + + onToggleChange(value: SendFilterType) { + const queryParams = value === SendFilterType.All ? { type: null } : { type: value }; + + this.router + .navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: "merge", + }) + .catch((err) => { + this.logService.error("Failed to update route query params:", err); + }); } } diff --git a/apps/web/src/app/tools/send/shared/index.ts b/apps/web/src/app/tools/send/shared/index.ts new file mode 100644 index 00000000000..afc507ee464 --- /dev/null +++ b/apps/web/src/app/tools/send/shared/index.ts @@ -0,0 +1 @@ +export { SendSuccessDrawerDialogComponent } from "./send-success-drawer-dialog.component"; diff --git a/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.html b/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.html new file mode 100644 index 00000000000..b9326ca08ac --- /dev/null +++ b/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.html @@ -0,0 +1,45 @@ + + + {{ dialogTitle() | i18n }} + + +
+
+
+ +
+
+ +

+ {{ "sendCreatedSuccessfully" | i18n }} +

+ +

+ {{ "sendCreatedDescription" | i18n: formattedExpirationTime }} +

+ + + {{ "sendLink" | i18n }} + + + +
+
+ + + + + +
diff --git a/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.ts b/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.ts new file mode 100644 index 00000000000..67e01cd9ff0 --- /dev/null +++ b/apps/web/src/app/tools/send/shared/send-success-drawer-dialog.component.ts @@ -0,0 +1,75 @@ +import { Component, ChangeDetectionStrategy, Inject, signal, computed } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { ActiveSendIcon } from "@bitwarden/assets/svg"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendType } from "@bitwarden/common/tools/send/types/send-type"; +import { DIALOG_DATA, DialogModule, ToastService, TypographyModule } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +@Component({ + imports: [SharedModule, DialogModule, TypographyModule], + templateUrl: "./send-success-drawer-dialog.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SendSuccessDrawerDialogComponent { + readonly sendLink = signal(""); + activeSendIcon = ActiveSendIcon; + + // Computed property to get the dialog title based on send type + readonly dialogTitle = computed(() => { + return this.send.type === SendType.Text ? "newTextSend" : "newFileSend"; + }); + + constructor( + @Inject(DIALOG_DATA) public send: SendView, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, + ) { + void this.initLink(); + } + + async initLink() { + const env = await firstValueFrom(this.environmentService.environment$); + this.sendLink.set(env.getSendUrl() + this.send.accessId + "/" + this.send.urlB64Key); + } + + get formattedExpirationTime(): string { + if (!this.send.deletionDate) { + return ""; + } + const hoursAvailable = this.getHoursAvailable(this.send); + if (hoursAvailable < 24) { + return hoursAvailable === 1 + ? this.i18nService.t("oneHour").toLowerCase() + : this.i18nService.t("durationTimeHours", String(hoursAvailable)).toLowerCase(); + } + const daysAvailable = Math.ceil(hoursAvailable / 24); + return daysAvailable === 1 + ? this.i18nService.t("oneDay").toLowerCase() + : this.i18nService.t("days", String(daysAvailable)).toLowerCase(); + } + + private getHoursAvailable(send: SendView): number { + const now = new Date().getTime(); + const deletionDate = new Date(send.deletionDate).getTime(); + return Math.max(0, Math.ceil((deletionDate - now) / (1000 * 60 * 60))); + } + + copyLink() { + const link = this.sendLink(); + if (!link) { + return; + } + this.platformUtilsService.copyToClipboard(link); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")), + }); + } +} diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html index 1976321b4ee..8cfd394b854 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html @@ -64,11 +64,11 @@

{{ "gettingStartedWithBitwardenPart1" | i18n }} - + {{ "gettingStartedWithBitwardenPart2" | i18n }} {{ "and" | i18n }} - + {{ "gettingStartedWithBitwardenPart3" | i18n }}

diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 3aa2f4b3bc1..640febf41d2 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -2,8 +2,9 @@ {{ title }} - @if (cipherIsArchived) { - {{ "archiveNoun" | i18n }} + + @if (isCipherArchived) { + {{ "archived" | i18n }} }
@@ -83,17 +84,39 @@ } - @if (showDelete) { + @if (showActionButtons) {
- + @if (userCanArchive$ | async) { + @if (isCipherArchived) { + + } + @if (cipher?.canBeArchived) { + + } + } + @if (cipher) { + + }
} 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 11862b569fc..1f430fea72e 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 @@ -1,25 +1,34 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { ActivatedRoute, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; 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"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { CipherRiskService } from "@bitwarden/common/vault/abstractions/cipher-risk.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { TaskService } from "@bitwarden/common/vault/tasks"; import { DialogRef, DIALOG_DATA, DialogService, ToastService } from "@bitwarden/components"; - -import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterService } from "@bitwarden/vault"; import { VaultItemDialogComponent } from "./vault-item-dialog.component"; @@ -33,7 +42,13 @@ class TestVaultItemDialogComponent extends VaultItemDialogComponent { this.params = params; } setTestCipher(cipher: any) { - this.cipher = cipher; + this.cipher = { + ...cipher, + login: { + uris: [], + }, + card: {}, + }; } setTestFormConfig(formConfig: any) { this.formConfig = formConfig; @@ -72,12 +87,23 @@ describe("VaultItemDialogComponent", () => { { provide: DIALOG_DATA, useValue: { ...baseParams } }, { provide: DialogRef, useValue: {} }, { provide: DialogService, useValue: {} }, - { provide: ToastService, useValue: {} }, + { + provide: ToastService, + useValue: { + showToast: () => {}, + }, + }, { provide: MessagingService, useValue: {} }, { provide: LogService, useValue: {} }, { provide: CipherService, useValue: {} }, - { provide: AccountService, useValue: { activeAccount$: { pipe: () => ({}) } } }, - { provide: ConfigService, useValue: { getFeatureFlag: () => Promise.resolve(false) } }, + { provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } }, + { + provide: ConfigService, + useValue: { + getFeatureFlag: () => Promise.resolve(false), + getFeatureFlag$: () => of(false), + }, + }, { provide: Router, useValue: {} }, { provide: ActivatedRoute, useValue: {} }, { @@ -89,8 +115,63 @@ describe("VaultItemDialogComponent", () => { { provide: ApiService, useValue: {} }, { provide: EventCollectionService, useValue: {} }, { provide: RoutedVaultFilterService, useValue: {} }, + { + provide: CipherArchiveService, + useValue: { + userCanArchive$: jest.fn().mockReturnValue(of(true)), + hasArchiveFlagEnabled$: jest.fn().mockReturnValue(of(true)), + archiveWithServer: jest.fn().mockResolvedValue({}), + unarchiveWithServer: jest.fn().mockResolvedValue({}), + }, + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: CollectionService, + useValue: mock(), + }, + { + provide: FolderService, + useValue: mock(), + }, + { + provide: TaskService, + useValue: mock(), + }, + { + provide: ApiService, + useValue: mock(), + }, + { + provide: EnvironmentService, + useValue: { + environment$: of({ + getIconsUrl: () => "https://example.com", + }), + }, + }, + { + provide: DomainSettingsService, + useValue: { + showFavicons$: of(true), + }, + }, + { + provide: BillingAccountProfileStateService, + useValue: { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(false)), + }, + }, + { + provide: PlatformUtilsService, + useValue: { + getClientType: jest.fn().mockReturnValue("Web"), + }, + }, { provide: SyncService, useValue: {} }, - { provide: PlatformUtilsService, useValue: {} }, + { provide: CipherRiskService, useValue: {} }, ], }).compileComponents(); @@ -140,10 +221,84 @@ describe("VaultItemDialogComponent", () => { expect(component.getTestTitle()).toBe("newItemHeaderCard"); }); }); + + describe("archive", () => { + it("calls archiveService to archive the cipher", async () => { + const archiveService = TestBed.inject(CipherArchiveService); + component.setTestCipher({ id: "111-222-333-4444" }); + component.setTestParams({ mode: "view" }); + fixture.detectChanges(); + + await component.archive(); + + expect(archiveService.archiveWithServer).toHaveBeenCalledWith("111-222-333-4444", "UserId"); + }); + }); + + describe("unarchive", () => { + it("calls archiveService to unarchive the cipher", async () => { + const archiveService = TestBed.inject(CipherArchiveService); + component.setTestCipher({ id: "111-222-333-4444" }); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + + await component.unarchive(); + + expect(archiveService.unarchiveWithServer).toHaveBeenCalledWith("111-222-333-4444", "UserId"); + }); + }); + + describe("archive button", () => { + it("should show archive button when the user can archive the item and the item can be archived", () => { + component.setTestCipher({ canBeArchived: true }); + (component as any).userCanArchive$ = of(true); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']")); + expect(archiveButton).toBeTruthy(); + }); + + it("should not show archive button when the user cannot archive the item", () => { + (component as any).userCanArchive$ = of(false); + component.setTestCipher({}); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']")); + expect(archiveButton).toBeFalsy(); + }); + + it("should not show archive button when the item cannot be archived", () => { + component.setTestCipher({ canBeArchived: false }); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']")); + expect(archiveButton).toBeFalsy(); + }); + }); + + describe("unarchive button", () => { + it("should show the unarchive button when the item is archived", () => { + component.setTestCipher({ isArchived: true }); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + const unarchiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-unarchive']")); + expect(unarchiveButton).toBeTruthy(); + }); + + it("should not show the unarchive button when the item is not archived", () => { + component.setTestCipher({ isArchived: false }); + component.setTestParams({ mode: "form" }); + fixture.detectChanges(); + const unarchiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-unarchive']")); + expect(unarchiveButton).toBeFalsy(); + }); + }); + describe("submitButtonText$", () => { it("should return 'unArchiveAndSave' when premium is false and cipher is archived", (done) => { jest.spyOn(component as any, "userHasPremium$", "get").mockReturnValue(of(false)); - component["cipherIsArchived"] = true; + component.setTestCipher({ isArchived: true }); + fixture.detectChanges(); component["submitButtonText$"].subscribe((text) => { expect(text).toBe("unArchiveAndSave"); @@ -153,7 +308,8 @@ describe("VaultItemDialogComponent", () => { it("should return 'save' when cipher is archived and user has premium", (done) => { jest.spyOn(component as any, "userHasPremium$", "get").mockReturnValue(of(true)); - component["cipherIsArchived"] = true; + component.setTestCipher({ isArchived: true }); + fixture.detectChanges(); component["submitButtonText$"].subscribe((text) => { expect(text).toBe("save"); @@ -163,7 +319,8 @@ describe("VaultItemDialogComponent", () => { it("should return 'save' when cipher is not archived", (done) => { jest.spyOn(component as any, "userHasPremium$", "get").mockReturnValue(of(false)); - component["cipherIsArchived"] = false; + component.setTestCipher({ isArchived: false }); + fixture.detectChanges(); component["submitButtonText$"].subscribe((text) => { expect(text).toBe("save"); 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 15e62eaf93e..5d5e319c8af 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 @@ -15,11 +15,11 @@ import { Router } from "@angular/router"; import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; -import { CollectionView } 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 { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -29,6 +29,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherId, CollectionId, OrganizationId } 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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; @@ -62,11 +63,11 @@ import { CipherViewComponent, DecryptionFailureDialogComponent, DefaultChangeLoginPasswordService, + RoutedVaultFilterService, + RoutedVaultFilterModel, } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; -import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; -import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; @@ -231,6 +232,18 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { ), ); + protected archiveFlagEnabled$ = this.archiveService.hasArchiveFlagEnabled$; + + protected userId$ = this.accountService.activeAccount$.pipe(getUserId); + + /** + * Flag to indicate if the user can archive items. + * @protected + */ + protected userCanArchive$ = this.userId$.pipe( + switchMap((userId) => this.archiveService.userCanArchive$(userId)), + ); + protected get isTrashFilter() { return this.filter?.type === "trash"; } @@ -243,6 +256,10 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.isTrashFilter && !this.showRestore; } + protected get showActionButtons() { + return this.cipher !== null && this.formConfig.mode !== "clone"; + } + /** * Determines if the user may restore the item. * A user may restore items if they have delete permissions and the item is in the trash. @@ -253,8 +270,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected showRestore: boolean; - protected cipherIsArchived: boolean = false; - protected get loadingForm() { return this.loadForm && !this.formReady; } @@ -267,15 +282,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.showCipherView && !this.isTrashFilter && !this.showRestore; } - protected get showDelete() { - // Don't show the delete button when cloning a cipher - if (this.params.mode == "form" && this.formConfig.mode === "clone") { - return false; - } - // Never show the delete button for new ciphers - return this.cipher != null; - } - protected get showCipherView() { return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm); } @@ -283,13 +289,17 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected get submitButtonText$(): Observable { return this.userHasPremium$.pipe( map((hasPremium) => - this.cipherIsArchived && !hasPremium + this.isCipherArchived && !hasPremium ? this.i18nService.t("unArchiveAndSave") : this.i18nService.t("save"), ), ); } + protected get isCipherArchived() { + return this.cipher?.isArchived; + } + /** * Flag to initialize/attach the form component. */ @@ -327,6 +337,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private apiService: ApiService, private eventCollectionService: EventCollectionService, private routedVaultFilterService: RoutedVaultFilterService, + private archiveService: CipherArchiveService, ) { this.updateTitle(); this.premiumUpgradeService.upgradeConfirmed$ @@ -339,7 +350,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { async ngOnInit() { this.cipher = await this.getDecryptedCipherView(this.formConfig); - if (this.cipher) { if (this.cipher.decryptionFailure) { this.dialogService.open(DecryptionFailureDialogComponent, { @@ -350,8 +360,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return; } - this.cipherIsArchived = this.cipher.isArchived; - this.collections = this.formConfig.collections.filter((c) => this.cipher.collectionIds?.includes(c.id), ); @@ -406,15 +414,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { cipherView.collectionIds?.includes(c.id), ); - // Track cipher archive state for btn text and badge updates - this.cipherIsArchived = this.cipher.isArchived; - // If the cipher was newly created (via add/clone), switch the form to edit for subsequent edits. if (this._originalFormMode === "add" || this._originalFormMode === "clone") { this.formConfig.mode = "edit"; this.formConfig.initialValues = null; } - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const activeUserId = await firstValueFrom(this.userId$); let cipher = await this.cipherService.get(cipherView.id, activeUserId); // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint (if not found in local state) @@ -508,9 +513,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { result.action === AttachmentDialogResult.Removed || result.action === AttachmentDialogResult.Uploaded ) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.userId$); let updatedCipherView: CipherView; @@ -562,11 +565,72 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { await this.changeMode("view"); }; + updateCipherFromArchive = (revisionDate: Date, archivedDate: Date | null) => { + this.cipher.archivedDate = archivedDate; + this.cipher.revisionDate = revisionDate; + + // If we're in View mode, we don't need to update the form. + if (this.params.mode === "view") { + return; + } + + this.cipherFormComponent.patchCipher((current) => { + current.revisionDate = revisionDate; + current.archivedDate = archivedDate; + return current; + }); + }; + + archive = async () => { + const activeUserId = await firstValueFrom(this.userId$); + try { + const cipherResponse = await this.archiveService.archiveWithServer( + this.cipher.id as CipherId, + activeUserId, + ); + this.updateCipherFromArchive( + new Date(cipherResponse.revisionDate), + cipherResponse.archivedDate ? new Date(cipherResponse.archivedDate) : null, + ); + + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("itemsWereSentToArchive"), + }); + } catch { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("errorOccurred"), + }); + } + }; + + unarchive = async () => { + const activeUserId = await firstValueFrom(this.userId$); + try { + const cipherResponse = await this.archiveService.unarchiveWithServer( + this.cipher.id as CipherId, + activeUserId, + ); + this.updateCipherFromArchive(new Date(cipherResponse.revisionDate), null); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("itemWasUnarchived"), + }); + } catch { + this.toastService.showToast({ + variant: "error", + message: this.i18nService.t("errorOccurred"), + }); + return; + } + }; + private async getDecryptedCipherView(config: CipherFormConfig) { if (config.originalCipher == null) { return; } - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const activeUserId = await firstValueFrom(this.userId$); return await this.cipherService.decrypt(config.originalCipher, activeUserId); } 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 9378ee54e51..49c9df8d582 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 @@ -142,4 +142,45 @@ describe("VaultCipherRowComponent", () => { expect(overlayContent).not.toContain('appcopyfield="password"'); }); }); + + describe("showAssignToCollections", () => { + let archivedCipher: CipherView; + + beforeEach(() => { + archivedCipher = new CipherView(); + archivedCipher.id = "cipher-1"; + archivedCipher.name = "Test Cipher"; + archivedCipher.type = CipherType.Login; + archivedCipher.organizationId = "org-1"; + archivedCipher.deletedDate = null; + archivedCipher.archivedDate = new Date(); + + component.cipher = archivedCipher; + component.organizations = [{ id: "org-1" } as any]; + component.canAssignCollections = true; + component.disabled = false; + }); + + it("returns true when cipher is archived and conditions are met", () => { + expect(component["showAssignToCollections"]).toBe(true); + }); + + it("returns false when cipher is deleted", () => { + archivedCipher.deletedDate = new Date(); + + expect(component["showAssignToCollections"]).toBe(false); + }); + + it("returns false when user cannot assign collections", () => { + component.canAssignCollections = false; + + expect(component["showAssignToCollections"]).toBe(false); + }); + + it("returns false when there are no organizations", () => { + component.organizations = []; + + expect(component["showAssignToCollections"]).toBeFalsy(); + }); + }); }); diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts index a723f1e942b..ec0fe42f927 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -11,7 +11,7 @@ import { input, } from "@angular/core"; -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -144,8 +144,9 @@ export class VaultCipherRowComponent implements OnInit } } + // Archive button will not show in Admin Console protected get showArchiveButton() { - if (!this.archiveEnabled()) { + if (!this.archiveEnabled() || this.viewingOrgVault) { return false; } @@ -216,11 +217,7 @@ export class VaultCipherRowComponent implements OnInit return CipherViewLikeUtils.decryptionFailure(this.cipher); } - // Do Not show Assign to Collections option if item is archived protected get showAssignToCollections() { - if (CipherViewLikeUtils.isArchived(this.cipher)) { - return false; - } return ( this.organizations?.length && this.canAssignCollections && diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index daa981d509a..9e6a589849f 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -7,7 +7,7 @@ import { Unassigned, CollectionView, CollectionTypes, -} from "@bitwarden/admin-console/common"; +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; 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 a4651da69e2..8cd4b98af40 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,4 +1,4 @@ -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { CollectionPermission } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-item.ts b/apps/web/src/app/vault/components/vault-items/vault-item.ts index bccb84fb0bf..27abca09eca 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-item.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-item.ts @@ -1,4 +1,4 @@ -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; export interface VaultItem { 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 c1c25c625da..ac74e75f07c 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 @@ -2,7 +2,7 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { TestBed } from "@angular/core/testing"; import { of, Subject } from "rxjs"; -import { CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -11,8 +11,7 @@ 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 } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service"; -import { RoutedVaultFilterModel } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; +import { RoutedVaultFilterService, RoutedVaultFilterModel } from "@bitwarden/vault"; import { VaultItem } from "./vault-item"; import { VaultItemsComponent } from "./vault-items.component"; @@ -79,6 +78,101 @@ describe("VaultItemsComponent", () => { component = fixture.componentInstance; }); + describe("bulkArchiveAllowed", () => { + it("returns false when no items are selected", () => { + component.userCanArchive = true; + component["selection"].clear(); + + expect(component.bulkArchiveAllowed).toBe(false); + }); + + it("returns false when userCanArchive is false", () => { + component.userCanArchive = false; + + const items: VaultItem[] = [ + { cipher: cipher1 as CipherView }, + { cipher: cipher2 as CipherView }, + ]; + + component["selection"].select(...items); + + expect(component.bulkArchiveAllowed).toBe(false); + }); + + it("returns false when selecting collections", () => { + component.userCanArchive = true; + const collection1 = { id: "col-1", name: "Collection 1" } as CollectionView; + + const items: VaultItem[] = [ + { cipher: cipher1 as CipherView }, + { collection: collection1 }, + ]; + + component["selection"].select(...items); + + expect(component.bulkArchiveAllowed).toBe(false); + }); + + it("returns true when selecting unarchived ciphers without organization", () => { + component.userCanArchive = true; + + const items: VaultItem[] = [ + { cipher: cipher1 as CipherView }, + { cipher: cipher2 as CipherView }, + ]; + + component["selection"].select(...items); + + expect(component.bulkArchiveAllowed).toBe(true); + }); + + it("returns false when any selected cipher has an organizationId", () => { + component.userCanArchive = true; + + const personalCipher: Partial = { + ...cipher1, + organizationId: undefined, + }; + + const orgCipher: Partial = { + ...cipher2, + organizationId: "org-1", + }; + + const items: VaultItem[] = [ + { cipher: personalCipher as CipherView }, + { cipher: orgCipher as CipherView }, + ]; + + component["selection"].select(...items); + + expect(component.bulkArchiveAllowed).toBe(false); + }); + + it("returns false when any selected cipher is already archived", () => { + component.userCanArchive = true; + + const unarchivedCipher: Partial = { + ...cipher1, + archivedDate: undefined, + }; + + const archivedCipher: Partial = { + ...cipher2, + archivedDate: new Date("2024-01-01"), + }; + + const items: VaultItem[] = [ + { cipher: unarchivedCipher as CipherView }, + { cipher: archivedCipher as CipherView }, + ]; + + component["selection"].select(...items); + + expect(component.bulkArchiveAllowed).toBe(false); + }); + }); + describe("bulkUnarchiveAllowed", () => { it("returns false when no items are selected", () => { component["selection"].clear(); 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 a51009a1e5b..7deaa2ff75e 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 @@ -13,7 +13,11 @@ import { switchMap, } from "rxjs"; -import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; +import { + CollectionAdminView, + Unassigned, + 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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; @@ -27,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/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterService } from "@bitwarden/vault"; import { GroupView } from "../../../admin-console/organizations/core"; @@ -272,7 +276,8 @@ export class VaultItemsComponent { } get bulkArchiveAllowed() { - if (this.selection.selected.length === 0 || !this.userCanArchive) { + const hasCollectionsSelected = this.selection.selected.some((item) => item.collection); + if (this.selection.selected.length === 0 || !this.userCanArchive || hasCollectionsSelected) { return false; } diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 9c56df0db59..76d6b460df0 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -11,13 +11,13 @@ import { } from "@storybook/angular"; import { BehaviorSubject, of } from "rxjs"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { CollectionAccessSelectionView, CollectionAdminView, Unassigned, -} from "@bitwarden/admin-console/common"; -import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; -import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -41,7 +41,7 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { LayoutComponent, StorybookGlobalStateProvider } from "@bitwarden/components"; import { GlobalStateProvider } from "@bitwarden/state"; -import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterService } from "@bitwarden/vault"; import { GroupView } from "../../../admin-console/organizations/core"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 5f139ade144..46f2b5da735 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -3,8 +3,9 @@ import { Component, Inject } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CollectionView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 19c462193e1..046def13d1a 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -3,7 +3,7 @@ import { Component, Input, OnChanges } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { Unassigned } from "@bitwarden/admin-console/common"; +import { Unassigned } from "@bitwarden/common/admin-console/models/collections"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index 2ba9dd6fad4..20148018c39 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from "@angular/core/testing"; -import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -17,11 +17,7 @@ import { import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { - PREMIUM_BANNER_REPROMPT_KEY, - VaultBannersService, - VisibleVaultBanner, -} from "./vault-banners.service"; +import { VaultBannersService, VisibleVaultBanner } from "./vault-banners.service"; describe("VaultBannersService", () => { let service: VaultBannersService; @@ -79,101 +75,6 @@ describe("VaultBannersService", () => { jest.useRealTimers(); }); - describe("Premium", () => { - it("waits until sync is completed before showing premium banner", async () => { - hasPremiumFromAnySource$.next(false); - isSelfHost.mockReturnValue(false); - lastSync$.next(null); - - service = TestBed.inject(VaultBannersService); - - const premiumBanner$ = service.shouldShowPremiumBanner$(userId); - - // Should not emit when sync is null - await expect(firstValueFrom(premiumBanner$.pipe(take(1), timeout(100)))).rejects.toThrow(); - - // Should emit when sync is completed - lastSync$.next(new Date("2024-05-14")); - expect(await firstValueFrom(premiumBanner$)).toBe(true); - }); - - it("does not show a premium banner for self-hosted users", async () => { - hasPremiumFromAnySource$.next(false); - isSelfHost.mockReturnValue(true); - - service = TestBed.inject(VaultBannersService); - - expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false); - }); - - it("does not show a premium banner when they have access to premium", async () => { - hasPremiumFromAnySource$.next(true); - isSelfHost.mockReturnValue(false); - - service = TestBed.inject(VaultBannersService); - - expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false); - }); - - describe("dismissing", () => { - beforeEach(async () => { - jest.useFakeTimers(); - const date = new Date("2023-06-08"); - date.setHours(0, 0, 0, 0); - jest.setSystemTime(date.getTime()); - - service = TestBed.inject(VaultBannersService); - await service.dismissBanner(userId, VisibleVaultBanner.Premium); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it("updates state on first dismiss", async () => { - const state = await firstValueFrom( - fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$, - ); - - const oneWeekLater = new Date("2023-06-15"); - oneWeekLater.setHours(0, 0, 0, 0); - - expect(state).toEqual({ - numberOfDismissals: 1, - nextPromptDate: oneWeekLater.getTime(), - }); - }); - - it("updates state on second dismiss", async () => { - const state = await firstValueFrom( - fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$, - ); - - const oneMonthLater = new Date("2023-07-08"); - oneMonthLater.setHours(0, 0, 0, 0); - - expect(state).toEqual({ - numberOfDismissals: 2, - nextPromptDate: oneMonthLater.getTime(), - }); - }); - - it("updates state on third dismiss", async () => { - const state = await firstValueFrom( - fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$, - ); - - const oneYearLater = new Date("2024-06-08"); - oneYearLater.setHours(0, 0, 0, 0); - - expect(state).toEqual({ - numberOfDismissals: 3, - nextPromptDate: oneYearLater.getTime(), - }); - }); - }); - }); - describe("OutdatedBrowser", () => { beforeEach(async () => { // Hardcode `MSIE` in userAgent string diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 1c53274d9d7..6371f78c0f5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@angular/core"; -import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -7,7 +7,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider, - PREMIUM_BANNER_DISK_LOCAL, BANNERS_DISMISSED_DISK, UserKeyDefinition, SingleUserState, @@ -18,30 +17,14 @@ import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; export const VisibleVaultBanner = { OutdatedBrowser: "outdated-browser", - Premium: "premium", VerifyEmail: "verify-email", PendingAuthRequest: "pending-auth-request", } as const; export type VisibleVaultBanner = UnionOfValues; -type PremiumBannerReprompt = { - numberOfDismissals: number; - /** Timestamp representing when to show the prompt next */ - nextPromptDate: number; -}; - /** Banners that will be re-shown on a new session */ -type SessionBanners = Omit; - -export const PREMIUM_BANNER_REPROMPT_KEY = new UserKeyDefinition( - PREMIUM_BANNER_DISK_LOCAL, - "bannerReprompt", - { - deserializer: (bannerReprompt) => bannerReprompt, - clearOn: [], // Do not clear user tutorials - }, -); +type SessionBanners = VisibleVaultBanner; export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition( BANNERS_DISMISSED_DISK, @@ -76,33 +59,6 @@ export class VaultBannersService { return pendingAuthRequests.length > 0 && !alreadyDismissed; } - shouldShowPremiumBanner$(userId: UserId): Observable { - const premiumBannerState = this.premiumBannerState(userId); - const premiumSources$ = combineLatest([ - this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId), - premiumBannerState.state$, - ]); - - return this.syncService.lastSync$(userId).pipe( - filter((lastSync) => lastSync !== null), - take(1), // Wait until the first sync is complete before considering the premium status - mergeMap(() => premiumSources$), - map(([canAccessPremium, dismissedState]) => { - const shouldShowPremiumBanner = - !canAccessPremium && !this.platformUtilsService.isSelfHost(); - - // Check if nextPromptDate is in the past passed - if (shouldShowPremiumBanner && dismissedState?.nextPromptDate) { - const nextPromptDate = new Date(dismissedState.nextPromptDate); - const now = new Date(); - return now >= nextPromptDate; - } - - return shouldShowPremiumBanner; - }), - ); - } - /** Returns true when the update browser banner should be shown */ async shouldShowUpdateBrowserBanner(userId: UserId): Promise { const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1; @@ -128,23 +84,11 @@ export class VaultBannersService { /** Dismiss the given banner and perform any respective side effects */ async dismissBanner(userId: UserId, banner: SessionBanners): Promise { - if (banner === VisibleVaultBanner.Premium) { - await this.dismissPremiumBanner(userId); - } else { - await this.sessionBannerState(userId).update((current) => { - const bannersDismissed = current ?? []; + await this.sessionBannerState(userId).update((current) => { + const bannersDismissed = current ?? []; - return [...bannersDismissed, banner]; - }); - } - } - - /** - * - * @returns a SingleUserState for the premium banner reprompt state - */ - private premiumBannerState(userId: UserId): SingleUserState { - return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY); + return [...bannersDismissed, banner]; + }); } /** @@ -161,42 +105,4 @@ export class VaultBannersService { // use nullish coalescing to default to an empty array return (await firstValueFrom(this.sessionBannerState(userId).state$)) ?? []; } - - /** Increment dismissal state of the premium banner */ - private async dismissPremiumBanner(userId: UserId): Promise { - await this.premiumBannerState(userId).update((current) => { - const numberOfDismissals = current?.numberOfDismissals ?? 0; - const now = new Date(); - - // Set midnight of the current day - now.setHours(0, 0, 0, 0); - - // First dismissal, re-prompt in 1 week - if (numberOfDismissals === 0) { - now.setDate(now.getDate() + 7); - return { - numberOfDismissals: 1, - nextPromptDate: now.getTime(), - }; - } - - // Second dismissal, re-prompt in 1 month - if (numberOfDismissals === 1) { - now.setMonth(now.getMonth() + 1); - return { - numberOfDismissals: 2, - nextPromptDate: now.getTime(), - }; - } - - // 3+ dismissals, re-prompt each year - // Avoid day/month edge cases and only increment year - const nextYear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate()); - nextYear.setHours(0, 0, 0, 0); - return { - numberOfDismissals: numberOfDismissals + 1, - nextPromptDate: nextYear.getTime(), - }; - }); - } } diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html index 44b2975ee19..f197853ac18 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.html @@ -44,15 +44,3 @@ (onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)" (onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)" > - - - {{ "premiumUpgradeUnlockFeatures" | i18n }} - - {{ "goPremium" | i18n }} - - diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts index 7730ab974fb..786f9a76a20 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { By } from "@angular/platform-browser"; import { RouterTestingModule } from "@angular/router/testing"; import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject } from "rxjs"; @@ -8,15 +7,13 @@ import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { BannerComponent, BannerModule } from "@bitwarden/components"; +import { BannerModule } from "@bitwarden/components"; import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component"; import { SharedModule } from "../../../shared"; @@ -30,11 +27,9 @@ describe("VaultBannersComponent", () => { let messageSubject: Subject<{ command: string }>; const premiumBanner$ = new BehaviorSubject(false); const pendingAuthRequest$ = new BehaviorSubject(false); - const PM24996_ImplementUpgradeFromFreeDialogFlag$ = new BehaviorSubject(false); const mockUserId = Utils.newGuid() as UserId; const bannerService = mock({ - shouldShowPremiumBanner$: jest.fn((userId: UserId) => premiumBanner$), shouldShowUpdateBrowserBanner: jest.fn(), shouldShowVerifyEmailBanner: jest.fn(), shouldShowPendingAuthRequestBanner: jest.fn((userId: UserId) => @@ -88,17 +83,6 @@ describe("VaultBannersComponent", () => { allMessages$: messageSubject.asObservable(), }), }, - { - provide: ConfigService, - useValue: mock({ - getFeatureFlag$: jest.fn((flag: FeatureFlag) => { - if (flag === FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog) { - return PM24996_ImplementUpgradeFromFreeDialogFlag$; - } - return new BehaviorSubject(false); - }), - }), - }, ], }) .overrideProvider(VaultBannersService, { useValue: bannerService }) @@ -112,53 +96,6 @@ describe("VaultBannersComponent", () => { fixture.detectChanges(); }); - describe("premiumBannerVisible$", () => { - beforeEach(() => { - // Reset feature flag to default (false) before each test - PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false); - }); - - it("shows premium banner when shouldShowPremiumBanner is true and feature flag is off", async () => { - premiumBanner$.next(true); - PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false); - - fixture.detectChanges(); - - const banner = fixture.debugElement.query(By.directive(BannerComponent)); - expect(banner.componentInstance.bannerType()).toBe("premium"); - }); - - it("hides premium banner when feature flag is enabled", async () => { - premiumBanner$.next(true); - PM24996_ImplementUpgradeFromFreeDialogFlag$.next(true); - - fixture.detectChanges(); - - const banner = fixture.debugElement.query(By.directive(BannerComponent)); - expect(banner).toBeNull(); - }); - - it("dismisses premium banner when shouldShowPremiumBanner is false", async () => { - premiumBanner$.next(false); - PM24996_ImplementUpgradeFromFreeDialogFlag$.next(false); - - fixture.detectChanges(); - - const banner = fixture.debugElement.query(By.directive(BannerComponent)); - expect(banner).toBeNull(); - }); - - it("hides premium banner when both shouldShowPremiumBanner is false and feature flag is enabled", async () => { - premiumBanner$.next(false); - PM24996_ImplementUpgradeFromFreeDialogFlag$.next(true); - - fixture.detectChanges(); - - const banner = fixture.debugElement.query(By.directive(BannerComponent)); - expect(banner).toBeNull(); - }); - }); - describe("determineVisibleBanner", () => { [ { diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index 80626d258f8..9ddfdedd61b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -1,14 +1,11 @@ import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { combineLatest, filter, firstValueFrom, map, Observable, switchMap } from "rxjs"; +import { filter, firstValueFrom, map } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { MessageListener } from "@bitwarden/common/platform/messaging"; -import { UserId } from "@bitwarden/common/types/guid"; import { BannerModule } from "@bitwarden/components"; import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; @@ -32,7 +29,6 @@ import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banner }) export class VaultBannersComponent implements OnInit { visibleBanners: VisibleVaultBanner[] = []; - premiumBannerVisible$: Observable; VisibleVaultBanner = VisibleVaultBanner; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @@ -45,23 +41,7 @@ export class VaultBannersComponent implements OnInit { private router: Router, private accountService: AccountService, private messageListener: MessageListener, - private configService: ConfigService, ) { - this.premiumBannerVisible$ = this.activeUserId$.pipe( - filter((userId): userId is UserId => userId != null), - switchMap((userId) => - combineLatest([ - this.vaultBannerService.shouldShowPremiumBanner$(userId), - this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog), - ]).pipe( - map( - ([shouldShowBanner, PM24996_ImplementUpgradeFromFreeDialogEnabled]) => - shouldShowBanner && !PM24996_ImplementUpgradeFromFreeDialogEnabled, - ), - ), - ), - ); - // Listen for auth request messages and show banner immediately this.messageListener.allMessages$ .pipe( diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 37b881406e3..d23fc7f958a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -32,12 +32,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { OrganizationFilter } from "@bitwarden/vault"; import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { EnrollMasterPasswordReset } from "../../../../admin-console/organizations/users/enroll-master-password-reset.component"; import { LinkSsoService } from "../../../../auth/core/services"; import { OptionsInput } from "../shared/components/vault-filter-section.component"; -import { OrganizationFilter } from "../shared/models/vault-filter.type"; // 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/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 8839fa5039d..234b227c76f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -27,22 +27,19 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; - -import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { + VaultFilterServiceAbstraction as VaultFilterService, VaultFilterList, VaultFilterSection, VaultFilterType, -} from "../shared/models/vault-filter-section.type"; -import { VaultFilter } from "../shared/models/vault-filter.model"; -import { + VaultFilter, CipherStatus, CipherTypeFilter, CollectionFilter, FolderFilter, OrganizationFilter, -} from "../shared/models/vault-filter.type"; +} from "@bitwarden/vault"; +import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { OrganizationOptionsComponent } from "./organization-options.component"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index 6ebbdc84c73..6443e143980 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -8,10 +8,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; - -import { VaultFilterService } from "../../services/abstractions/vault-filter.service"; -import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type"; -import { VaultFilter } from "../models/vault-filter.model"; +import { + VaultFilterServiceAbstraction as VaultFilterService, + VaultFilterSection, + VaultFilterType, + VaultFilter, +} 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 diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts index dc70561bcb2..4d98bcd42bc 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts @@ -1,14 +1,13 @@ import { NgModule } from "@angular/core"; import { SearchModule } from "@bitwarden/components"; +import { VaultFilterServiceAbstraction, VaultFilterService } from "@bitwarden/vault"; import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module"; import { OrganizationOptionsComponent } from "./components/organization-options.component"; import { VaultFilterComponent } from "./components/vault-filter.component"; -import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service"; -import { VaultFilterService } from "./services/vault-filter.service"; @NgModule({ imports: [VaultFilterSharedModule, SearchModule, OrganizationWarningsModule], diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 8fa801f5dc0..e7c14825fe2 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -3,13 +3,13 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from import { Router } from "@angular/router"; import { firstValueFrom, switchMap } from "rxjs"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Unassigned, CollectionView, - CollectionAdminService, CollectionTypes, -} from "@bitwarden/admin-console/common"; -import { JslibModule } from "@bitwarden/angular/jslib.module"; +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -24,16 +24,12 @@ import { MenuModule, SimpleDialogOptions, } from "@bitwarden/components"; -import { NewCipherMenuComponent } from "@bitwarden/vault"; +import { NewCipherMenuComponent, All, RoutedVaultFilterModel } from "@bitwarden/vault"; import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog"; import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; import { PipesModule } from "../pipes/pipes.module"; -import { - All, - RoutedVaultFilterModel, -} from "../vault-filter/shared/models/routed-vault-filter.model"; @Component({ selector: "app-vault-header", diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index df1b727154f..cb5332d07d8 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -43,7 +43,9 @@
{{ "premiumSubscriptionEndedDesc" | i18n }}
- {{ "restartPremium" | i18n }} + + {{ "restartPremium" | i18n }} + } 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 a5121831304..cad2c97557b 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -25,14 +25,7 @@ import { tap, } from "rxjs/operators"; -import { - AutomaticUserConfirmationService, - CollectionData, - CollectionDetailsResponse, - CollectionService, - CollectionView, - Unassigned, -} from "@bitwarden/admin-console/common"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { NoResults, @@ -42,6 +35,7 @@ import { ItemTypes, Icon, } from "@bitwarden/assets/svg"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { @@ -50,7 +44,17 @@ import { } 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 { + CollectionDetailsResponse, + CollectionView, + Unassigned, + CollectionData, +} from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { + getNestedCollectionTree, + getFlatCollectionTree, +} from "@bitwarden/common/admin-console/utils"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -97,6 +101,15 @@ import { DecryptionFailureDialogComponent, DefaultCipherFormConfigService, PasswordRepromptService, + VaultFilterServiceAbstraction as VaultFilterService, + RoutedVaultFilterBridgeService, + RoutedVaultFilterService, + createFilterFunction, + All, + RoutedVaultFilterModel, + VaultFilter, + FolderFilter, + OrganizationFilter, VaultItemsTransferService, DefaultVaultItemsTransferService, } from "@bitwarden/vault"; @@ -104,10 +117,6 @@ import { UnifiedUpgradePromptService } from "@bitwarden/web-vault/app/billing/in import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; -import { - getNestedCollectionTree, - getFlatCollectionTree, -} from "../../admin-console/organizations/collections"; import { AutoConfirmPolicy, AutoConfirmPolicyDialogComponent, @@ -140,16 +149,6 @@ import { } from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component"; import { VaultBannersComponent } from "./vault-banners/vault-banners.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; -import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; -import { RoutedVaultFilterBridgeService } from "./vault-filter/services/routed-vault-filter-bridge.service"; -import { RoutedVaultFilterService } from "./vault-filter/services/routed-vault-filter.service"; -import { createFilterFunction } from "./vault-filter/shared/models/filter-function"; -import { - All, - RoutedVaultFilterModel, -} from "./vault-filter/shared/models/routed-vault-filter.model"; -import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; -import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component"; diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index c6a7c9c830d..bd97ed4ea55 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -1,18 +1,18 @@ import { TestBed } from "@angular/core/testing"; import { BehaviorSubject, of } from "rxjs"; -import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.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 { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { CollectionAdminView } from "@bitwarden/common/admin-console/models/collections"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; - -import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterService } from "@bitwarden/vault"; import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service"; diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 939729568e9..01a2f23f4e1 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -16,9 +16,12 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault"; - -import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { + CipherFormConfig, + CipherFormConfigService, + CipherFormMode, + RoutedVaultFilterService, +} from "@bitwarden/vault"; /** Admin Console implementation of the `CipherFormConfigService`. */ @Injectable() diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 76f130bc4d5..32a3baa4de0 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Daar is geen gebeure om te lys nie." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Geaktiveer" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Sleutel" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send geskep", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gewysig", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Herroep" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send-skakel", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Onttrek van rekeningterugstel" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index c794d8f48a0..573d5aa5b01 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "ليس هناك أعضاء لعرضهم." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "لا توجد أية أحداث لعرضها." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "تم التفعيل" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "استعادة الوصول" }, @@ -2634,6 +2640,9 @@ "key": { "message": "المفتاح" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "رمز التحقق" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "إرسال محفوظ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "إرسال محفوظ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "ملغاة" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "إرسال رابط", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index a86dbdb6406..b1e1ec33b28 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Sadalanacaq heç bir üzv yoxdur." }, + "noMembersToExport": { + "message": ".dillonvince767@gmail.com" + }, "noEventsInList": { "message": "Sadalanacaq heç bir tədbir yoxdur." }, @@ -2140,7 +2143,7 @@ "message": "Davam etsəniz, hazırkı seansınız bitəcək, təkrar giriş etməyiniz tələb olunacaq. Digər cihazlardakı aktiv seanslar, bir saata qədər aktiv qalmağa davam edə bilər." }, "changePasswordWarning": { - "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saat ərzində çıxış sonlandırılacaq." + "message": "Parolunuzu dəyişdirdikdən sonra yeni parolunuzla giriş etməli olacaqsınız. Digər cihazlardakı aktiv sessiyalar bir saat ərzində sonlandırılacaq." }, "emailChanged": { "message": "E-poçt dəyişdirildi" @@ -2537,6 +2540,9 @@ "enabled": { "message": "Fəallaşdırıldı" }, + "optionEnabled": { + "message": "Fəaldır" + }, "restoreAccess": { "message": "Erişimi bərpa et" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Açar" }, + "unnamedKey": { + "message": "Adsız açar" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Doğrulama kodu" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Arxivinizə təkrar erişmək üçün Premium abunəliyinizi yenidən başladın. Təkrar başlatmazdan əvvəl arxivlənmiş elementin detallarına düzəliş etsəniz, həmin element seyfinizə daşınacaq." }, + "itemRestored": { + "message": "Element bərpa edildi" + }, "restartPremium": { "message": "\"Premium\"u yenidən başlat" }, @@ -5604,6 +5616,37 @@ "message": "Send yaradıldı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send uğurla yaradıldı!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Yeni Send mətni", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Yeni Send faylı", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "\"Send\"ə düzəliş edildi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Ləğv edildi" }, + "accepted": { + "message": "Qəbul edildi" + }, "sendLink": { "message": "\"Send\" keçidi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Hesab geri qaytarılmasına yazıldınız" }, + "enrolled": { + "message": "Yazılma uğurludur" + }, + "notEnrolled": { + "message": "Yazılma baş tutmayıb" + }, "withdrawAccountRecovery": { "message": "Hesab geri qaytarılması üzrə razılığı geri götür" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu giriş risk altındadır və bir veb sayt əskikdir. Daha güclü təhlükəsizlik üçün bir veb sayt əlavə edin və parolu dəyişdirin." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Əskik veb sayt" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Arxivdən çıxart" }, + "archived": { + "message": "Arxivləndi" + }, "unArchiveAndSave": { "message": "Arxivdən çıxart və saxla" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Elementlər arxivə göndərildi" }, + "itemWasUnarchived": { + "message": "Element arxivdən çıxarıldı" + }, "itemUnarchived": { "message": "Element arxivdən çıxarıldı" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "İstifadəçi doğrulaması uğursuz oldu." }, + "resizeSideNavigation": { + "message": "Yan naviqasiyanı yeni. ölçüləndir" + }, "recoveryDeleteCiphersTitle": { "message": "Geri qaytarıla bilməyən seyf elementlərini sil" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Tam onlayn təhlükəsizlik" + }, + "updatePayment": { + "message": "Ödənişi güncəllə" + }, + "weCouldNotProcessYourPayment": { + "message": "Ödənişiniz emal olunmadı. Lütfən ödəniş üsulunuzu güncəlləyin, ya da kömək üçün dəstək komandası ilə əlaqə saxlayın." + }, + "yourSubscriptionHasExpired": { + "message": "Abunəliyinizin vaxtı bitib. Kömək üçün lütfən dəstək komandası ilə əlaqə saxlayın." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Abunəliyiniz $DATE$ tarixində ləğv ediləcək. Abunəliyinizi bu tarixdən əvvəl təkrar aktivləşdirə bilərsiniz.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "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." + }, + "youHaveAGracePeriod": { + "message": "Abunəliyinizin bitmə tarixindən etibarən $DAYS$ gün güzəşt dövrünüz var. Lütfən $DATE$ tarixinə qədər vaxtı keçmiş fakturaları həll edin.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Fakturaları idarə et" + }, + "yourNextChargeIsFor": { + "message": "Növbəti ödəniş haqqı" + }, + "dueOn": { + "message": "tarix" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Abunəliyinizin fəaliyyəti dayandırılacaq" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Abunəliyinizin fəaliyyəti dayandırıldı" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Abunəliyiniz ləğv ediləcək" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Abunəliyiniz ləğv edildi" + }, + "storageFull": { + "message": "Anbar dolub" + }, + "storageUsedDescription": { + "message": "$AVAILABLE$ GB şifrələnmiş fayl anbarınızın $USED$ qədəri istifadə olunub.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "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." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index f0f595e9b63..5fc19a5cb81 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "У спісе адсутнічаюць удзельнікі." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "У спісе адсутнічаюць падзеі." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Уключана" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Аднавіць доступ" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ключ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Створаны Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send адрэдагаваны", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Адклікана" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Спасылка на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Зарэгістравацца на аднаўленне ўліковага запісу" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Адклікаць аднаўленне ўліковага запісу" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 31a91a195ad..cd0f219e47a 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Няма членове за показване." }, + "noMembersToExport": { + "message": "Няма членове за изнасяне." + }, "noEventsInList": { "message": "Няма събития за показване." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Включено" }, + "optionEnabled": { + "message": "Включено" + }, "restoreAccess": { "message": "Възстановяване на достъпа" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ключ" }, + "unnamedKey": { + "message": "Ключ без име" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Код за потвърждаване" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Ако искате отново да получите достъп до архива си, трябва да подновите платения си абонамент. Ако редактирате данните за архивиран елемент преди подновяването, той ще бъде върнат в трезора." }, + "itemRestored": { + "message": "Записът бе възстановен" + }, "restartPremium": { "message": "Подновяване на платения абонамент" }, @@ -5604,6 +5616,37 @@ "message": "Създадено изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Изпращането е създадено успешно!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ часа", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Ново текстово Изпращане", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Ново файлово Изпращане", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Редактирано изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Отнет достъп" }, + "accepted": { + "message": "Прието" + }, "sendLink": { "message": "Изпращане на връзката", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Включен във възстановяването на профили" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Оттегляне от възстановяването на профили" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Този елемент за вписване е в риск и в него липсва уеб сайт. Добавете уеб сайт и сменете паролата, за по-добра сигурност." }, + "vulnerablePassword": { + "message": "Уязвима парола." + }, + "changeNow": { + "message": "Промяна сега" + }, "missingWebsite": { "message": "Липсващ уеб сайт" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Изваждане от архива" }, + "archived": { + "message": "Архивирано" + }, "unArchiveAndSave": { "message": "Разархивиране и запазване" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Елементите бяха преместени в архива" }, + "itemWasUnarchived": { + "message": "Елементът беше изваден от архива" + }, "itemUnarchived": { "message": "Елементът беше изваден от архива" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Проверката на потребителя беше неуспешна." }, + "resizeSideNavigation": { + "message": "Преоразмеряване на страничната навигация" + }, "recoveryDeleteCiphersTitle": { "message": "Изтриване на невъзстановимите елементи от трезора" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Пълна сигурност в Интернет" + }, + "updatePayment": { + "message": "Актуализиране на плащанията" + }, + "weCouldNotProcessYourPayment": { + "message": "Плащането не беше успешно. Моля, актуализирайте разплащателния си метод или се свържете с екипа по поддръжката за съдействие." + }, + "yourSubscriptionHasExpired": { + "message": "Абонаментът Ви е изтекъл. Моля, свържете се с екипа по поддръжката за съдействие." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Абонаментът Ви по план ще бъде преустановен на $DATE$. Можете да го подновите по всяко време преди това.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Споделяйте още повече със Семейния план, или преминете към подсилената защита на паролите с Екипния план или този за големи организации." + }, + "youHaveAGracePeriod": { + "message": "След като просрочите периода на абонамента си, разполагате с още $DAYS$ дни,. Моля, заплатете старите фактури до $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Управление на фактурите" + }, + "yourNextChargeIsFor": { + "message": "Следващото Ви таксуване ще бъде" + }, + "dueOn": { + "message": "с крайна дата" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Абонаментът Ви ще бъде спрян на" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Абонаментът Ви беше спрян на" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Абонаментът Ви ще бъде прекратен на" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Абонаментът Ви беше прекратен на" + }, + "storageFull": { + "message": "Mястото за съхранение е пълно" + }, + "storageUsedDescription": { + "message": "Използвали сте $USED$ от $AVAILABLE$ GB от наличното си място за съхранение на шифровани данни.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Използвали сте всичките си $GB$ GB от наличното си място за съхранение на шифровани данни. Ако искате да продължите да добавяте файлове, добавете повече място за съхранение." + }, + "whenYouRemoveStorage": { + "message": "Когато премахнете съхранението, ще получите пропорционално задължение към акаунта си, което ще бъде включено автоматично в следващата Ви сметка." + }, + "youHavePremium": { + "message": "Имате платен абонамент" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index a7558a78856..1d3901581ab 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "লিঙ্ক পাঠান", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index f287b526aa2..aef132431e9 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Nema događaja za prikaz." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 47cd621647a..190cdfe9aa4 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "No hi ha cap membre a llistar." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "No hi ha cap esdeveniment a llistar." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Habilitat" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restaura l'accés" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Clau" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Codi de verificació" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send creat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send guardat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revocat" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Enllaç Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Inscrit en la recuperació del compte" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Retirar-se de la recuperació del compte" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 2cb6547205d..0ea73451e1d 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Žádní členové k zobrazení." }, + "noMembersToExport": { + "message": "Žádní členové pro export." + }, "noEventsInList": { "message": "Žádné události k zobrazení." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Zapnuto" }, + "optionEnabled": { + "message": "Povoleno" + }, "restoreAccess": { "message": "Obnovit přístup" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Klíč" }, + "unnamedKey": { + "message": "Nepojmenovaný klíč" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Ověřovací kód" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Chcete-li získat přístup k Vašemu archivu, restartujte předplatné Premium. Pokud upravíte detaily archivované položky před restartováním, bude přesunuta zpět do Vašeho trezoru." }, + "itemRestored": { + "message": "Položka byla obnovena" + }, "restartPremium": { "message": "Restartovat Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send byl uložen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send byl úspěšně vytvořen!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hodin", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Nový textový Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Nový Send se soubory", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send byl uložen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Odvoláno" }, + "accepted": { + "message": "Přijato" + }, "sendLink": { "message": "Odkaz pro Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Zapsán do obnovení účtu" }, + "enrolled": { + "message": "Zapsán" + }, + "notEnrolled": { + "message": "Nezapsán" + }, "withdrawAccountRecovery": { "message": "Odstoupit z obnovení účtu" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Tyto přihlašovací údaje jsou ohrožené a chybí jim webová stránka. Přidejte webovou stránku a změňte heslo pro větší bezpečnost." }, + "vulnerablePassword": { + "message": "Zranitelné heslo." + }, + "changeNow": { + "message": "Změnit nyní" + }, "missingWebsite": { "message": "Chybějící webová stránka" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Odebrat z archivu" }, + "archived": { + "message": "Archivováno" + }, "unArchiveAndSave": { "message": "Odebrat z archivu a uložit" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Položky byly přesunuty do archivu" }, + "itemWasUnarchived": { + "message": "Položka byla odebrána z archivu" + }, "itemUnarchived": { "message": "Položka byla odebrána z archivu" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Ověření uživatele se nezdařilo." }, + "resizeSideNavigation": { + "message": "Změnit velikost boční navigace" + }, "recoveryDeleteCiphersTitle": { "message": "Smazat neobnovitelné položky trezoru" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Dokončit online zabezpečení" + }, + "updatePayment": { + "message": "Aktualizovat platbu" + }, + "weCouldNotProcessYourPayment": { + "message": "Nemohli jsme zpracovat Vaši platbu. Aktualizujte způsob platby nebo kontaktujte tým podpory pro pomoc." + }, + "yourSubscriptionHasExpired": { + "message": "Vaše předplatné vypršelo. Kontaktujte tým podpory pro pomoc." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Zrušení Vašeho předplatného je naplánováno na $DATE$. Před tím ho můžete kdykoli obnovit.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Sdílejte ještě více s rodinami, nebo získejte mocné, důvěryhodné heslo s týmy nebo Enterprise." + }, + "youHaveAGracePeriod": { + "message": "Máte lhůtu k odkladu $DAYS$ dnů od data vypršení předplatného. Vyřešte poslední splatné faktury do $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Spravovat faktury" + }, + "yourNextChargeIsFor": { + "message": "Vaše další platba je za" + }, + "dueOn": { + "message": "splatná" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Vaše předplatné bude pozastaveno" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Vaše předplatné bylo pozastaveno dne" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Vaše předplatné bude zrušeno dne" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Vaše předplatné bylo zrušeno dne" + }, + "storageFull": { + "message": "Úložiště je plné" + }, + "storageUsedDescription": { + "message": "Využili jste $USED$ z $AVAILABLE$ GB Vašeho šifrovaného úložiště.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "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ě." + }, + "whenYouRemoveStorage": { + "message": "Když odeberete úložiště, obdržíte kredit, který bude automaticky převeden do Vašeho dalšího vyúčtování." + }, + "youHavePremium": { + "message": "Máte Premium" + }, + "emailProtected": { + "message": "E-mail je chráněný" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 087d353b3a4..da522125f28 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 53f20b8a8f7..05868c77e85 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Ingen medlemmer at vise." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Der er ingen begivenheder at vise." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Aktiveret" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Gendan adgang" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Nøgle" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Bekræftelseskode" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send gemt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gemt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Tilbagekaldt" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send-link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Indrulleret i kontogendannelse" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Afmeld fra kontogendannelse" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index ae95c0ca9cb..c200857fb47 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Keine Mitglieder zum Anzeigen vorhanden." }, + "noMembersToExport": { + "message": "Es gibt keine Mitglieder zum Exportieren." + }, "noEventsInList": { "message": "Keine Ereignisse vorhanden." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Aktiviert" }, + "optionEnabled": { + "message": "Aktiviert" + }, "restoreAccess": { "message": "Zugriff wiederherstellen" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Schlüssel" }, + "unnamedKey": { + "message": "Unbenannter Schlüssel" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verifizierungscode" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Starte dein Premium-Abonnement neu, um den Zugriff auf dein Archiv wiederherzustellen. Wenn du die Details für einen archivierten Eintrag vor dem Neustart bearbeitest, wird er wieder zurück in deinen Tresor verschoben." }, + "itemRestored": { + "message": "Eintrag wurde wiederhergestellt" + }, "restartPremium": { "message": "Premium neu starten" }, @@ -4212,10 +4224,10 @@ } }, "userAcceptedTransfer": { - "message": "Accepted transfer to organization ownership." + "message": "Eigentumsübertragung an die Organisation akzeptiert." }, "userDeclinedTransfer": { - "message": "Revoked for declining transfer to organization ownership." + "message": "Widerrufen wegen Ablehnung der Übertragung an die Organisation." }, "invitedUserId": { "message": "Benutzer $ID$ eingeladen.", @@ -5604,6 +5616,37 @@ "message": "Send gespeichert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send erfolgreich erstellt!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ Stunden", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Neues Text-Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Neues Datei-Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gespeichert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Widerrufen" }, + "accepted": { + "message": "Akzeptiert" + }, "sendLink": { "message": "Send-Link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Für Kontowiederherstellung registriert" }, + "enrolled": { + "message": "Registriert" + }, + "notEnrolled": { + "message": "Nicht registriert" + }, "withdrawAccountRecovery": { "message": "Von Kontowiederherstellung abmelden" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Diese Zugangsdaten sind gefährdet und es fehlt eine Website. Füge eine Website hinzu und ändere das Passwort für mehr Sicherheit." }, + "vulnerablePassword": { + "message": "Gefährdetes Passwort." + }, + "changeNow": { + "message": "Jetzt ändern" + }, "missingWebsite": { "message": "Fehlende Webseite" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Nicht mehr archivieren" }, + "archived": { + "message": "Archiviert" + }, "unArchiveAndSave": { "message": "Nicht mehr archivieren und speichern" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Einträge wurden ins Archiv verschoben" }, + "itemWasUnarchived": { + "message": "Eintrag wird nicht mehr archiviert" + }, "itemUnarchived": { "message": "Eintrag wird nicht mehr archiviert" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Benutzerverifizierung fehlgeschlagen." }, + "resizeSideNavigation": { + "message": "Größe der Seitennavigation ändern" + }, "recoveryDeleteCiphersTitle": { "message": "Nicht-wiederherstellbare Tresor-Einträge löschen" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Umfassende Online-Sicherheit" + }, + "updatePayment": { + "message": "Zahlungsmethode aktualisieren" + }, + "weCouldNotProcessYourPayment": { + "message": "Wir konnten deine Zahlung nicht verarbeiten. Bitte aktualisiere deine Zahlungsmethode oder wende dich an das Support-Team, um Hilfe zu erhalten." + }, + "yourSubscriptionHasExpired": { + "message": "Dein Abonnement ist abgelaufen. Bitte kontaktiere das Support-Team für Hilfe." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Dein Abonnement wird am $DATE$ gekündigt. Du kannst es davor jederzeit wieder reaktivieren.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Teile noch mehr mit Families oder erhalte eine leistungsstarke und vertrauenswürdige Passwortsicherheit mit Teams oder Enterprise." + }, + "youHaveAGracePeriod": { + "message": "Du hast eine Nachfrist von $DAYS$ Tagen ab Ablaufdatum deines Abonnements. Bitte begleiche die letzten fälligen Rechnungen bis zum $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Rechnungen verwalten" + }, + "yourNextChargeIsFor": { + "message": "Deine nächste Abbuchung erfolgt am" + }, + "dueOn": { + "message": "fällig am" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Dein Abonnement wird deaktiviert am" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Dein Abonnement wurde deaktiviert am" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Dein Abonnement wird gekündigt am" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Dein Abonnement wurde gekündigt am" + }, + "storageFull": { + "message": "Speicherplatz voll" + }, + "storageUsedDescription": { + "message": "Du hast $USED$ von $AVAILABLE$ GB deines verschlüsselten Datenspeichers verwendet.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "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." + }, + "whenYouRemoveStorage": { + "message": "Wenn du Speicherplatz entfernst, erhältst du eine anteilige Gutschrift, die automatisch mit deiner nächsten Rechnung verrechnet wird." + }, + "youHavePremium": { + "message": "Du hast Premium" + }, + "emailProtected": { + "message": "E-Mail-Adresse geschützt" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 9232935b33c..3d0bc52b9b2 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Δεν υπάρχουν μέλη προς εμφάνιση." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Δεν υπάρχουν γεγονότα στη λίστα." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Ενεργοποιημένο" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Επαναφορά πρόσβασης" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Κλειδί" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Κωδικός επαλήθευσης" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Το Send Δημιουργήθηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Το Send Επεξεργάστηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Ανακλήθηκαν" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Αποστολή Συνδέσμου", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Έγινε εγγραφή στην ανάκτηση λογαριασμού" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Απόσυρση από την ανάκτηση λογαριασμού" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 98f847e1d36..ecad9f8a624 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1188,7 +1188,7 @@ "message": "Me" }, "myItems": { - "message": "My items" + "message": "My Items" }, "myVault": { "message": "My vault" @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -3146,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -3272,7 +3281,7 @@ "nextChargeHeader": { "message": "Next Charge" }, - "plan": { + "plan": { "message": "Plan" }, "details": { @@ -3670,9 +3679,6 @@ "defaultCollection": { "message": "Default collection" }, - "myItems": { - "message": "My Items" - }, "getHelp": { "message": "Get help" }, @@ -4497,7 +4503,6 @@ "updateBrowser": { "message": "Update browser" }, - "generatingYourAccessIntelligence": { "message": "Generating your Access Intelligence..." }, @@ -5611,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5650,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5885,22 +5924,22 @@ "message": "credential lifecycle", "description": "This will be used as a hyperlink" }, - "organizationDataOwnershipWarningTitle":{ + "organizationDataOwnershipWarningTitle": { "message": "Are you sure you want to proceed?" }, - "organizationDataOwnershipWarning1":{ + "organizationDataOwnershipWarning1": { "message": "will remain accessible to members" }, - "organizationDataOwnershipWarning2":{ + "organizationDataOwnershipWarning2": { "message": "will not be automatically selected when creating new items" }, - "organizationDataOwnershipWarning3":{ + "organizationDataOwnershipWarning3": { "message": "cannot be managed from the Admin Console until the user is offboarded" }, - "organizationDataOwnershipWarningContentTop":{ + "organizationDataOwnershipWarningContentTop": { "message": "By turning this policy off, the default collection: " }, - "organizationDataOwnershipWarningContentBottom":{ + "organizationDataOwnershipWarningContentBottom": { "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, @@ -6024,7 +6063,7 @@ "uriMatchDetectionOptionsLabel": { "message": "Default URI match detection" }, - "invalidUriMatchDefaultPolicySetting": { + "invalidUriMatchDefaultPolicySetting": { "message": "Please select a valid URI match detection option.", "description": "Error message displayed when a user attempts to save URI match detection policy settings with an invalid selection." }, @@ -6308,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -8934,7 +8979,7 @@ } }, "accessedSecret": { - "message": "Accessed secret $SECRET_ID$.", + "message": "Accessed secret $SECRET_ID$.", "placeholders": { "secret_id": { "content": "$1", @@ -8942,7 +8987,7 @@ } } }, - "editedSecretWithId": { + "editedSecretWithId": { "message": "Edited a secret with identifier: $SECRET_ID$", "placeholders": { "secret_id": { @@ -8951,7 +8996,7 @@ } } }, - "deletedSecretWithId": { + "deletedSecretWithId": { "message": "Deleted a secret with identifier: $SECRET_ID$", "placeholders": { "secret_id": { @@ -8969,7 +9014,7 @@ } } }, - "restoredSecretWithId": { + "restoredSecretWithId": { "message": "Restored a secret with identifier: $SECRET_ID$", "placeholders": { "secret_id": { @@ -8978,7 +9023,7 @@ } } }, - "createdSecretWithId": { + "createdSecretWithId": { "message": "Created a new secret with identifier: $SECRET_ID$", "placeholders": { "secret_id": { @@ -8988,7 +9033,7 @@ } }, "accessedProjectWithIdentifier": { - "message": "Accessed a project with identifier: $PROJECT_ID$.", + "message": "Accessed a project with identifier: $PROJECT_ID$.", "placeholders": { "project_id": { "content": "$1", @@ -8997,7 +9042,7 @@ } }, "nameUnavailableProjectDeleted": { - "message": "Deleted project Id: $PROJECT_ID$", + "message": "Deleted project Id: $PROJECT_ID$", "placeholders": { "project_id": { "content": "$1", @@ -9006,7 +9051,7 @@ } }, "nameUnavailableSecretDeleted": { - "message": "Deleted secret Id: $SECRET_ID$", + "message": "Deleted secret Id: $SECRET_ID$", "placeholders": { "secret_id": { "content": "$1", @@ -9015,7 +9060,7 @@ } }, "nameUnavailableServiceAccountDeleted": { - "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", + "message": "Deleted machine account Id: $SERVICE_ACCOUNT_ID$", "placeholders": { "service_account_id": { "content": "$1", @@ -9023,7 +9068,7 @@ } } }, - "editedProjectWithId": { + "editedProjectWithId": { "message": "Edited a project with identifier: $PROJECT_ID$", "placeholders": { "project_id": { @@ -9102,7 +9147,7 @@ } } }, - "deletedProjectWithId": { + "deletedProjectWithId": { "message": "Deleted a project with identifier: $PROJECT_ID$", "placeholders": { "project_id": { @@ -9111,7 +9156,7 @@ } } }, - "createdProjectWithId": { + "createdProjectWithId": { "message": "Created a new project with identifier: $PROJECT_ID$", "placeholders": { "project_id": { @@ -9829,15 +9874,15 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "uriMatchDefaultStrategyHint": { + "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, - "regExAdvancedOptionWarning": { + "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", "description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy" }, - "startsWithAdvancedOptionWarning": { + "startsWithAdvancedOptionWarning": { "message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.", "description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy" }, @@ -9845,11 +9890,11 @@ "message": "More about match detection", "description": "Link to match detection docs on warning dialog for advance match strategy" }, - "uriAdvancedOption":{ + "uriAdvancedOption": { "message": "Advanced options", "description": "Advanced option placeholder for uri option component" }, - "warningCapitalized": { + "warningCapitalized": { "message": "Warning", "description": "Warning (should maintain locale-relevant capitalization)" }, @@ -11574,6 +11619,12 @@ }, "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." + }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" }, "missingWebsite": { "message": "Missing website" @@ -11613,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11631,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12187,9 +12244,6 @@ "updateYourEncryptionSettings": { "message": "Update your encryption settings" }, - "updateSettings": { - "message": "Update settings" - }, "algorithm": { "message": "Algorithm" }, @@ -12260,7 +12314,7 @@ } } }, - "removeMasterPasswordForOrgUserKeyConnector":{ + "removeMasterPasswordForOrgUserKeyConnector": { "message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain." }, "continueWithLogIn": { @@ -12278,10 +12332,10 @@ "verifyYourOrganization": { "message": "Verify your organization to log in" }, - "organizationVerified":{ + "organizationVerified": { "message": "Organization verified" }, - "domainVerified":{ + "domainVerified": { "message": "Domain verified" }, "leaveOrganizationContent": { @@ -12415,7 +12469,7 @@ } } }, - "howToManageMyVault": { + "howToManageMyVault": { "message": "How do I manage my vault?" }, "transferItemsToOrganizationTitle": { @@ -12445,7 +12499,7 @@ "whyAmISeeingThis": { "message": "Why am I seeing this?" }, - "youHaveBitwardenPremium": { + "youHaveBitwardenPremium": { "message": "You have Bitwarden Premium" }, "viewAndManagePremiumSubscription": { @@ -12463,7 +12517,7 @@ } } }, - "uploadLicenseFile": { + "uploadLicenseFile": { "message": "Upload license file" }, "uploadYourLicenseFile": { @@ -12481,7 +12535,7 @@ } } }, - "alreadyHaveSubscriptionQuestion": { + "alreadyHaveSubscriptionQuestion": { "message": "Already have a subscription?" }, "alreadyHaveSubscriptionSelfHostedMessage": { @@ -12490,7 +12544,90 @@ "viewAllPlans": { "message": "View all plans" }, - "planDescPremium":{ + "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 3e25235b9a8..b9ff96a119f 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be cancelled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was cancelled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index b65b8d40622..6fbee21406e 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Enabled" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Created Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Edited Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send Link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be cancelled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was cancelled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 7ee4e504035..b1b2ee4dae9 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Estas neniu membro por listi." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Estas neniu evento por listi." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Ŝaltita" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Rehavigi aliron" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Konigilo" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Aŭtentiga kodo" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Kreita Sendo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Redaktita Sendo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Sendi ligilon", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 68141386a0e..6d6895eaaba 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "No hay miembros para mostrar." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "No hay eventos que listar." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Activado" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Recuperar el acceso" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Clave" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Código de verificación" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send creado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send editado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revocado" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Enlace Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Inscrito en la recuperación de la cuenta" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Retirarse de la recuperación de la cuenta" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index d05ba09dfbd..722b715aa0b 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Puuduvad kasutajad, keda kuvada." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Puuduvad sündmused, mida kuvada." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Sisselülitatud" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Taasta ligipääsu luba" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Võti" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Kinnituskood" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send on loodud", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Muutis Sendi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Eemaldatud" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Sendi link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 39473dbc75c..f05450a68b2 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Ez dago erakusteko gertakaririk." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Gaituta" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Sarbidea berreskuratu" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Gakoa" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send-a sortua", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send-a editatua", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Ezeztatuak" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send esteka", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index b85c692e1fe..5a05f9b44dc 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "هیچ عضوی برای نمایش وجود ندارد." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "هیچ مناسبتی برای نمایش وجود ندارد." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "روشن شده" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "بازیابی دسترسی" }, @@ -2634,6 +2640,9 @@ "key": { "message": "کلید" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "کد تأیید" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "ارسال ذخیره شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "ارسال ذخیره شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "لغو شد" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "ارسال پیوند", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "در بازیابی حساب کاربری ثبت نام شد" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "برداشت از بازیابی حساب" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 871b256877a..8cbe7cffb11 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Näytettäviä jäseniä ei ole." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Näytettäviä tapahtumia ei ole." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Käytössä" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Palauta käyttöoikeudet" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Avain" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Vahvistuskoodi" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send tallennettiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send tallennettiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Mitätöidyt" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send-linkki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Liitetty tilin palautusapuun" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Eroa tilin palautusavusta" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index a12aa97341f..3e73d3e8332 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Walang maililistang miyembro." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Walang maililistang kaganapan." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Nakabukas" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Ibalik ang access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Ipadala na nilikha", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Ipadala na nai-save", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Binawi ang" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Ipadala ang link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 582d3492a42..5326586a660 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Il n'y a pas de membres à répertorier." }, + "noMembersToExport": { + "message": "Il n'y a aucun membre à exporter." + }, "noEventsInList": { "message": "Aucun événement à afficher." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Activé" }, + "optionEnabled": { + "message": "Activé" + }, "restoreAccess": { "message": "Restaurer l'Accès" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Clé" }, + "unnamedKey": { + "message": "Clé sans nom" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Code de vérification" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "L'élément a été restauré" + }, "restartPremium": { "message": "Redémarrer Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send créé", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send créé avec succès !", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "message": "Copiez et partagez ce lien Send. Il peut être consulté par les personnes que vous avez spécifiées pour $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ heures", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Nouveau texte Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Nouveau fichier Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send modifié", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Révoqué" }, + "accepted": { + "message": "Accepté" + }, "sendLink": { "message": "Lien du Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Inscrit à la récupération du compte" }, + "enrolled": { + "message": "Inscrit" + }, + "notEnrolled": { + "message": "Non inscrit" + }, "withdrawAccountRecovery": { "message": "Retirer de la récupération du compte" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "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": "Mot de passe vulnérable." + }, + "changeNow": { + "message": "Changer maintenant" + }, "missingWebsite": { "message": "Site Web manquant" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Désarchiver" }, + "archived": { + "message": "Archivé" + }, "unArchiveAndSave": { "message": "Désarchiver et enregistrer" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Les éléments ont été envoyés à l'archive" }, + "itemWasUnarchived": { + "message": "L'élément a été désarchivé" + }, "itemUnarchived": { "message": "L'élément a été désarchivé" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "La vérification de l'utilisateur a échoué." }, + "resizeSideNavigation": { + "message": "Redimensionner la navigation latérale" + }, "recoveryDeleteCiphersTitle": { "message": "Supprimer les éléments non récupérables du coffre" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Sécurité en ligne complète" + }, + "updatePayment": { + "message": "Mettre à jour le paiement" + }, + "weCouldNotProcessYourPayment": { + "message": "Nous n'avons pas pu traiter votre paiement. Veuillez mettre à jour votre méthode de paiement ou contactez l'équipe de support pour obtenir de l'assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Votre abonnement a expiré. Veuillez contacter l'équipe de suuport pour obtenir de l'assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Votre abonnement est programmé pour terminer le $DATE$. Vous pouvez le rétablir à tout moment avant cette date.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Partagez encore plus avec Familles, ou obtenez une sécurité de mot de passe puissante et fiable avec Équipes ou Entreprise." + }, + "youHaveAGracePeriod": { + "message": "Vous bénéficiez d'une période de grace de $DAYS$ jours suivants la date d'expiration de votre abonnement. Veuillez régler les paiements en souffrance pour les factures dont les échéances sont passées d'ici le $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Gérer les factures" + }, + "yourNextChargeIsFor": { + "message": "Votre prochaine charge est de" + }, + "dueOn": { + "message": "dû le" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Votre abonnement sera suspendu le" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Votre abonnement a été suspendu le" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Votre abonnement sera annulé le" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Votre abonnement a été annulé le" + }, + "storageFull": { + "message": "Stockage plein" + }, + "storageUsedDescription": { + "message": "Vous avez utilisé $USED$ sur $AVAILABLE$ Go de votre stockage de fichiers chiffré.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Vous avez utilisé tous les $GB$ Go de votre stockage chiffré. Pour continuer à stocker des fichiers, ajoutez plus de stockage." + }, + "whenYouRemoveStorage": { + "message": "Lorsque vous supprimez le stockage, vous recevrez un crédit de compte au prorata qui sera automatiquement appliqué à votre prochaine facture." + }, + "youHavePremium": { + "message": "Vous avez Premium" + }, + "emailProtected": { + "message": "Protégé par courriel" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 7c555a3f142..7a1b82865a8 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index c0e1995289e..b602fcbe9e6 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "אין חברים להצגה ברשימה." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "אין אירועים להצגה ברשימה." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "מופעל" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "שחזר גישה" }, @@ -2634,6 +2640,9 @@ "key": { "message": "מפתח" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "קוד אימות" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "סֵנְד נשמר", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "סֵנְד נשמר", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "מבוטל" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "קישור סֵנְד", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "נרשם לשחזור חשבון" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "לסגת משחזור חשבון" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "כניסה זו נמצאת בסיכון וחסר לה אתר אינטרנט. הוסף אתר אינטרנט ושנה את הסיסמה עבור אבטחה חזקה יותר." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "אתר אינטרנט חסר" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "הסר מהארכיון" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "פריטים שנשלחו לארכיון" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "הפריט הוסר מהארכיון" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 4b578f69e5c..60f5b3465d6 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 92f0d020812..56169d3c8e9 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Nema članova za prikaz." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Nema događaja za prikaz." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Omogućeno" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Vrati pristup" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ključ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Kôd za provjeru" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Za ponovni pristup svojoj arhivi, ponovno pokreni Premium pretplatu. Ako urediš detalje arhivirane stavke prije ponovnog pokretanja, ona će biti vraćena u tvoj trezor." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Ponovno Pokreni Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send stvoren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send uređen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Opozvano" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Veza na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Oporavak računa uključen" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Isključi oporavak računa" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ova prijava je ugrožena i nedostaje joj web-stranica. Dodaj web-stranicu i promijeni lozinku za veću sigurnost." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Nedostaje web-stranica" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Poništi arhiviranje" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Stavke poslane u arhivu" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Stavka vraćena iz arhive" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index dff04ac5b3b..6c8ca99c5e8 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Nincsenek megjeleníthető tagok." }, + "noMembersToExport": { + "message": "Nincsenek exportálható tagok." + }, "noEventsInList": { "message": "Nincsenek megjeleníthető események." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Engedélyezve" }, + "optionEnabled": { + "message": "Engedélyezve" + }, "restoreAccess": { "message": "Hozzáférés helyreállítás" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Kulcs" }, + "unnamedKey": { + "message": "Névtelen kulcs" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Ellenőrző kód" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Az archívumhoz hozzáférés visszaszerzéséhez indítsuk újra a Prémium előfizetést. Ha az újraindítás előtt szerkesztjük egy archivált elem adatait, akkor az visszakerül a széfbe." }, + "itemRestored": { + "message": "Az elem visszaállításra került." + }, "restartPremium": { "message": "Prémium előfizetés újraindítása" }, @@ -5604,6 +5616,37 @@ "message": "A Send mentésre került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "A Send sikeresen létrejött!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ óra", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Új szöveges Send elem", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Új fájl Send elem", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "A Send szerkesztésre került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "A visszavonás megtörtént." }, + "accepted": { + "message": "Elfogadva" + }, "sendLink": { "message": "Send hivatkozás", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Megtörtént a regisztráció fiók helyreállításra." }, + "enrolled": { + "message": "Feliratkozott" + }, + "notEnrolled": { + "message": "Nem feliratkozott" + }, "withdrawAccountRecovery": { "message": "Kilépés a fiók helyreállításból" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ez a bejelentkezés veszélyben van és hiányzik egy webhely. Adjunk hozzá egy webhelyet és módosítsuk a jelszót az erősebb biztonság érdekében." }, + "vulnerablePassword": { + "message": "A jelszó sérülékeny." + }, + "changeNow": { + "message": "Módosítás most" + }, "missingWebsite": { "message": "Hiányzó webhely" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Visszavétel archívumból" }, + "archived": { + "message": "Archiválva" + }, "unArchiveAndSave": { "message": "Archiválás visszavonása és mentés" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Az elemek az archivumba kerültek." }, + "itemWasUnarchived": { + "message": "Az elem visszavételre került az archivumból." + }, "itemUnarchived": { "message": "Az elemek visszavéelre kerültek az archivumból." }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "A felhasználó ellenőrzése sikertelen volt." }, + "resizeSideNavigation": { + "message": "Oldalnavigáció átméretezés" + }, "recoveryDeleteCiphersTitle": { "message": "Helyreállíthatatlan széf elemek törlése" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Teljes körű online biztonság" + }, + "updatePayment": { + "message": "Fizetés frissítése" + }, + "weCouldNotProcessYourPayment": { + "message": "Nem lehetett feldolgozni a fizetést. Frissítsük a fizetési módot vagy forduljunk segítségért az ügyfélszolgálathoz." + }, + "yourSubscriptionHasExpired": { + "message": "Az előfizetés lejárt. Forduljunk a támogató csapathoz segítségért." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Az előfizetés az időzítés szerint lejár: $DATE$. Ezt megelőzően bármikor visszaállíthatjuk.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Osszunk meg még többet a Családi csomaggal vagy kapjunk hatékony, megbízható jelszóbiztonságot a Teams vagy az Enterprise segítségével." + }, + "youHaveAGracePeriod": { + "message": "Az előfizetés lejárati dátumától számítva $DAYS$ nap türelmi időszak áll rendelkezésére az előfizetés fenntartásához. Rendezzük a lejárt számlákat a következő időpontig: $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Számlák kezelése" + }, + "yourNextChargeIsFor": { + "message": "Következő terhelés" + }, + "dueOn": { + "message": "esedékes" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Az előfizetés felfüggesztésre kerül:" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Az előfizetés felfüggesztésre került:" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Az előfizetés törlésre kerül:" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Az előfizetés törlésre került:" + }, + "storageFull": { + "message": "A tárhely megtelt." + }, + "storageUsedDescription": { + "message": "$USED$ / $AVAILABLE$ GB lett felhasználva a titkosított fájl tárolóból.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "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." + }, + "whenYouRemoveStorage": { + "message": "A tárhely eltávolításakor arányos számlajóváírást kapunk, amely automatikusan a következő számlára kerül." + }, + "youHavePremium": { + "message": "Prémium felhasználó vagyunk" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index e349f4180b9..4c158999da8 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -179,16 +179,16 @@ } }, "noDataInOrgTitle": { - "message": "No data found" + "message": "Tidak ada data ditemukan" }, "noDataInOrgDescription": { "message": "Import your organization's login data to get started with Access Intelligence. Once you do that, you'll be able to:" }, "feature1Title": { - "message": "Mark applications as critical" + "message": "Tandai aplikasi sebagai penting" }, "feature1Description": { - "message": "This will help you remove risks to your most important applications first." + "message": "Ini membantu menghilangkan risiko pada apl terpenting Anda terlebih dahulu." }, "feature2Title": { "message": "Help members improve their security" @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Tidak ada anggota untuk ditampilkan." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Tidak ada acara untuk dicantumkan." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Diaktifkan" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Pulihkan Akses" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Kunci" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Kode verifikasi" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Kirim disimpan", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Kirim disimpan", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Dicabut" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Kirim Tautan", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index b4942bd75f6..1a0435b4f04 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Nessun membro da mostrare." }, + "noMembersToExport": { + "message": "Nessun elemento da esportare." + }, "noEventsInList": { "message": "Nessun evento da mostrare." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Attivato" }, + "optionEnabled": { + "message": "Attivo" + }, "restoreAccess": { "message": "Ripristina accesso" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Chiave" }, + "unnamedKey": { + "message": "Chiave senza nome" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Codice di verifica" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Per recuperare l'accesso al tuo archivio, riavvia il tuo abbonamento Premium. Se modifichi i dettagli di un elemento archiviato prima del riavvio, sarà spostato nella tua cassaforte." }, + "itemRestored": { + "message": "L'elemento è stato ripristinato" + }, "restartPremium": { "message": "Riavvia Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send salvato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send creato con successo!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "message": "Copia e condividi questo link Send: potrà essere visualizzato dalle persone che hai specificato per le prossime $TIME$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ ore", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Nuovo Send testuale", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Nuovo Send con file", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send salvato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revocato" }, + "accepted": { + "message": "Approvato" + }, "sendLink": { "message": "Link del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Iscritto al recupero dell'account" }, + "enrolled": { + "message": "Registrato" + }, + "notEnrolled": { + "message": "Non registrato" + }, "withdrawAccountRecovery": { "message": "Rifiuta il recupero dell'account" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Questo login è a rischio e non contiene un sito web. Aggiungi un sito web e cambia la password per maggiore sicurezza." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Sito web mancante" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Togli dall'archivio" }, + "archived": { + "message": "Archiviato" + }, "unArchiveAndSave": { "message": "Togli dall'archivio e salva" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Elementi archiviati" }, + "itemWasUnarchived": { + "message": "Elemento rimosso dall'archivio" + }, "itemUnarchived": { "message": "Elemento rimosso dall'archivio" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Verifica dell'utente non riuscita." }, + "resizeSideNavigation": { + "message": "Ridimensiona la navigazione laterale" + }, "recoveryDeleteCiphersTitle": { "message": "Elimina gli oggetti della cassaforte non recuperabili" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Sicurezza online completa" + }, + "updatePayment": { + "message": "Aggiorna il metodo di pagamento" + }, + "weCouldNotProcessYourPayment": { + "message": "Non è stato possibile elaborare la transazione. Aggiorna il metodo di pagamento o contatta l'assistenza." + }, + "yourSubscriptionHasExpired": { + "message": "Il tuo abbonamento è scaduto. Contatta il team di supporto per ricevere assistenza." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Il tuo abbonamento è programmato per terminare il $DATE$. Puoi ripristinarlo in qualsiasi momento prima di quella data.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Condividi ancora di più con il piano famiglie, o rafforza le protezioni con Team o Enterprise." + }, + "youHaveAGracePeriod": { + "message": "Hai un periodo di grazia di $DAYS$ giorni dalla fine dell'abbonamento. Ti preghiamo di provvedere ai pagamenti entro il $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Gestisci le fatture" + }, + "yourNextChargeIsFor": { + "message": "Il tuo prossimo addebito è programmato" + }, + "dueOn": { + "message": "per" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Il tuo abbonamento sarà sospeso il" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Il tuo abbonamento è stato sospeso il" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Il tuo abbonamento terminerà il" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Il tuo abbonamento è terminato il" + }, + "storageFull": { + "message": "Spazio di archiviazione pieno" + }, + "storageUsedDescription": { + "message": "Hai usato $USED$ GB su $AVAILABLE$ del tuo archivio file crittografato.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Hai usato tutti i $GB$ GB del tuo spazio di archiviazione crittografato. Per archiviare altri file, aggiungi altro spazio." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 20fa1137002..0dbf162ae09 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "表示できるメンバーがいません。" }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "表示するイベントがありません" }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "有効化されました" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "アクセスを復元する" }, @@ -2634,6 +2640,9 @@ "key": { "message": "キー" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "認証コード" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "作成した Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "編集済みの Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "取り消し済み" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send リンク", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "アカウント回復に登録しました" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "アカウント回復から登録解除する" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 4a6927d9f3a..d8e3088794d 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "არაა წევრები ჩამოსათველად." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "არაა მოვლენები ჩამოსათველად." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index f570d355369..b352e56ba4f 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 94237cb1028..88ada3bb0bb 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "ಪಟ್ಟಿ ಮಾಡಲು ಯಾವುದೇ ಘಟನೆಗಳಿಲ್ಲ." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "ಸಕ್ರಿಯಗೊಳಿಸಿದೆ" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "ಕೀ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "ಕಳುಹಿಸು ರಚಿಸಲಾಗಿದೆ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "ಕಳುಹಿಸಿದ ಸಂಪಾದನೆ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "ಲಿಂಕ್ ಕಳುಹಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index c0471507369..ededdc008ff 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "이벤트가 없습니다." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "활성화됨" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "키" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send 생성함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 수정함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send 링크", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index eb39c3b8eee..1d382a9533c 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Nav dalībnieku, ko uzskaitīt." }, + "noMembersToExport": { + "message": "Nav dalībnieku, ko izgūt." + }, "noEventsInList": { "message": "Nav notikumu, ko parādīt." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Iespējots" }, + "optionEnabled": { + "message": "Iespējots" + }, "restoreAccess": { "message": "Atjaunot piekļuvi" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Atslēga" }, + "unnamedKey": { + "message": "Nenodēvēta atslēga" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Apliecinājuma kods" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Lai atgūtu piekļuvi savam arhīvam, jāatsāk Premium abonements. Ja labosi arhivēta vienuma informāciju pirms atsākšanas, tas tiks pārvietots atpakaļ Tavā glabātavā." }, + "itemRestored": { + "message": "Vienums tika atjaunots" + }, "restartPremium": { "message": "Atsākt Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saglabāts", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send tika veiksmīgi izveidots.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "message": "Ievieto starpliktuvē un kopīgo šī Send saiti! To $TIME$ no šī brīža var apskatīt cilvēki, kurus norādīji.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "time": { + "content": "$1", + "example": "7 days" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ stundas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Jauns teksta Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Jauns datnes Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saglabāts", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Atsauktie" }, + "accepted": { + "message": "Pieņemts" + }, "sendLink": { "message": "Send saite", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Pieteicies konta atkopei" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Atsaukt konta atkopi" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Šis pieteikšanās vienums ir pakļauts riskam, un tam nav norādīta tīmekļvietne. Lielākai drošībai jāpievieno tīmekļvietne un jānomaina parole." }, + "vulnerablePassword": { + "message": "Ievainojama parole." + }, + "changeNow": { + "message": "Mainīt tagad" + }, "missingWebsite": { "message": "Nav norādīta tīmekļvietne" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Atcelt arhivēšanu" }, + "archived": { + "message": "Arhivēts" + }, "unArchiveAndSave": { "message": "Atcelt arhivēšanu un saglabāt" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Vienumi tika ievietoti arhīvā" }, + "itemWasUnarchived": { + "message": "Vienums tika izņemts no arhīva" + }, "itemUnarchived": { "message": "Vienums tika izņemts no arhīva" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Lietotāja apliecināšana neizdevās." }, + "resizeSideNavigation": { + "message": "Mainīt sānu pārvietošanās joslas izmēru" + }, "recoveryDeleteCiphersTitle": { "message": "Izdzēst neatkopjamos glabātavas vienumus" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Pilnīga drošība tiešsaistē" + }, + "updatePayment": { + "message": "Atjaunināt maksājumu" + }, + "weCouldNotProcessYourPayment": { + "message": "Mēs nevarējām apstrādāt maksājumu. Lūgums atjaunināt savu maksājumu veidu vai sazināties ar atbalsta komandu, lai iegūtu palīdzību." + }, + "yourSubscriptionHasExpired": { + "message": "Abonements ir beidzies. Lūgums sazināties ar atbalsta komandu, lai iegūtu palīdzību." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Abonements ir ierindots atcelšanai $DATE$. Līdz tam jebkurā brīdī to var atjaunot.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "Kad noņemsi krātuvi, saņemsi konta kredītu noteiktā apjomā, kas tiks automātiski izmantots nākamajā rēķinā." + }, + "youHavePremium": { + "message": "Tev ir Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 1dc40cce28b..5ab54c63025 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "പ്രദർശിപ്പിക്കാൻ ഇവന്റുകളൊന്നുമില്ല." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "പ്രവർത്തനക്ഷമമാക്കി" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send സൃഷ്‌ടിച്ചു", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send തിരുത്തി", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "ലിങ്ക് അയയ്‌ക്കുക", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index b51c66468c3..3a0696f8dbb 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index f570d355369..b352e56ba4f 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index dd76b11f8be..455650f4144 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Det er ingen medlemmer å vise." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Det er ingen hendelser å liste opp." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Aktivert" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Gjenopprett tilgang" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Nøkkel" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verifiseringskode" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Laget Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Endret Send-en", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Opphevet" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send lenke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index cfd5c682a99..9d6333810ce 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 61538a1bb2a..0c0cafd1a8a 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1747,7 +1747,10 @@ "message": "Er zijn geen gebruikers om weer te geven." }, "noMembersInList": { - "message": "Er zijn geen leden op weer te geven." + "message": "Er zijn geen leden om weer te geven." + }, + "noMembersToExport": { + "message": "Er zijn geen leden om te exporteren." }, "noEventsInList": { "message": "Er zijn geen gebeurtenissen om weer te geven." @@ -2537,6 +2540,9 @@ "enabled": { "message": "Ingeschakeld" }, + "optionEnabled": { + "message": "Ingeschakeld" + }, "restoreAccess": { "message": "Toegang herstellen" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Sleutel" }, + "unnamedKey": { + "message": "Naamloze sleutel" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verificatiecode" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Herstart je Premium-abonnement om toegang tot je archief te krijgen. Als je de details wijzigt voor een gearchiveerd item voor het opnieuw opstarten, zal het terug naar je kluis worden verplaatst." }, + "itemRestored": { + "message": "Item is hersteld" + }, "restartPremium": { "message": "Premium herstarten" }, @@ -5604,6 +5616,37 @@ "message": "Send aangemaakt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send bewerkt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Ingetrokken" }, + "accepted": { + "message": "Geaccepteerd" + }, "sendLink": { "message": "Send-koppeling", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Aangemeld bij accountherstel" }, + "enrolled": { + "message": "Ingeschreven" + }, + "notEnrolled": { + "message": "Niet ingeschreven" + }, "withdrawAccountRecovery": { "message": "Afmelden voor accountherstel" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Deze login is in gevaar en mist een website. Voeg een website toe en verander het wachtwoord voor een sterkere beveiliging." }, + "vulnerablePassword": { + "message": "Kwetsbaar wachtwoord." + }, + "changeNow": { + "message": "Nu wijzigen" + }, "missingWebsite": { "message": "Ontbrekende website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Dearchiveren" }, + "archived": { + "message": "Gearchiveerd" + }, "unArchiveAndSave": { "message": "Dearchiveren en opslaan" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Item naar archief verzonden" }, + "itemWasUnarchived": { + "message": "Item uit het archief gehaald" + }, "itemUnarchived": { "message": "Item uit het archief gehaald" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Formaat zijnavigatie wijzigen" + }, "recoveryDeleteCiphersTitle": { "message": "Onherstelbare kluisitems verwijderen" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Online beveiliging voltooien" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "Wanneer je opslag verwijdert, krijg je op je volgende rekening automatisch pro-rata rekeningkrediet." + }, + "youHavePremium": { + "message": "Je hebt Premium" + }, + "emailProtected": { + "message": "E-mail beveiligd" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index d7cfdd21d2f..a0f21867e4b 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Det er ingen hendingar å syna." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Nykel" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index f570d355369..b352e56ba4f 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 93843ca45b7..d70ddc8490d 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Brak członków do wyświetlenia." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Brak wydarzeń do wyświetlenia." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Włączone" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Przywróć dostęp" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Klucz" }, + "unnamedKey": { + "message": "Klucz bez nazwy" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Kod weryfikacyjny" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Wysyłka została zapisana", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Wysyłka została zapisana", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Unieważnieni" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Link wysyłki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Dołączono do odzyskiwania konta" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Wycofaj z odzyskiwania konta" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Zmień rozmiar nawigacji bocznej" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 28e95ed6379..846eb6edd98 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -551,7 +551,7 @@ } }, "autoFillOnPageLoad": { - "message": "Preencher automaticamente ao carregar a página?" + "message": "Preencher ao carregar a página?" }, "number": { "message": "Número" @@ -1050,7 +1050,7 @@ "description": "Header for view SSH key item type" }, "new": { - "message": "Novo", + "message": "Criar", "description": "for adding new items" }, "item": { @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Não há membros para listar." }, + "noMembersToExport": { + "message": "Não há membros para exportar." + }, "noEventsInList": { "message": "Não há eventos para listar." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Ativado" }, + "optionEnabled": { + "message": "Ativado" + }, "restoreAccess": { "message": "Restaurar acesso" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Chave" }, + "unnamedKey": { + "message": "Chave sem nome" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Código de verificação" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Para recuperar o seu acesso ao seu arquivo, retoma sua assinatura Premium. Se editar detalhes de um item arquivado antes de retomar, ele será movido de volta para o seu cofre." }, + "itemRestored": { + "message": "O item foi restaurado" + }, "restartPremium": { "message": "Retomar Premium" }, @@ -4215,7 +4227,7 @@ "message": "Aceitou a transferência da propriedade da organização." }, "userDeclinedTransfer": { - "message": "Não aceitou a transferência da propriedade da organização." + "message": "Revogado por não aceitar a transferência da propriedade da organização." }, "invitedUserId": { "message": "Convidou o usuário $ID$.", @@ -5604,6 +5616,37 @@ "message": "Send salvo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send criado com sucesso!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Novo Send de texto", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Novo Send de arquivo", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send salvo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revogados" }, + "accepted": { + "message": "Aceito" + }, "sendLink": { "message": "Link do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Inscrito na recuperação de conta" }, + "enrolled": { + "message": "Inscrito" + }, + "notEnrolled": { + "message": "Não inscrito" + }, "withdrawAccountRecovery": { "message": "Retirar-se da recuperação de conta" }, @@ -9777,7 +9829,7 @@ "message": "Instalar extensão de navegador" }, "installBrowserExtensionDetails": { - "message": "Use a extensão para salvar credenciais e preencher automaticamente formulários de forma rápida, sem abrir o aplicativo web." + "message": "Use a extensão para salvar credenciais e preencher formulários de forma rápida, sem abrir o aplicativo web." }, "projectAccessUpdated": { "message": "Acesso ao projeto atualizado" @@ -11075,7 +11127,7 @@ "message": "Use campos ocultos para dados sensíveis como senhas" }, "checkBoxHelpText": { - "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um lembrar e-mail" + "message": "Use caixas de seleção se gostaria de preencher a caixa de seleção de um formulário, como um lembrar e-mail" }, "linkedHelpText": { "message": "Use um campo vinculado quando estiver enfrentando problemas com o preenchimento automático em um site específico." @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e está sem um site. Adicione um site e altere a senha para segurança melhor." }, + "vulnerablePassword": { + "message": "Senha vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site ausente" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Desarquivar" }, + "archived": { + "message": "Arquivados" + }, "unArchiveAndSave": { "message": "Desarquivar e salvar" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Itens foram enviados para o arquivo" }, + "itemWasUnarchived": { + "message": "O item foi desarquivado" + }, "itemUnarchived": { "message": "O item foi desarquivado" }, @@ -11743,7 +11807,7 @@ "message": "Adicionar mais tarde" }, "cannotAutofillPasswordsWithoutExtensionTitle": { - "message": "Você não pode preencher senhas automaticamente sem a extensão de navegador" + "message": "Você não pode preencher senhas sem a extensão de navegador" }, "cannotAutofillPasswordsWithoutExtensionDesc": { "message": "Você tem certeza de que não quer adicionar a extensão agora?" @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Falha na verificação do usuário." }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" + }, "recoveryDeleteCiphersTitle": { "message": "Apagar itens não recuperáveis do cofre" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Segurança on-line completa" + }, + "updatePayment": { + "message": "Atualizar pagamento" + }, + "weCouldNotProcessYourPayment": { + "message": "Não pudemos processar seu pagamento. Atualize seu método de pagamento ou entre em contato com a equipe de suporte para assistência." + }, + "yourSubscriptionHasExpired": { + "message": "Sua assinatura expirou. Entre em contato com a equipe de suporte para obter assistência." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Sua assinatura está agendada para ser cancelada em $DATE$. Você pode reestabelecê-la a qualquer momento antes disso.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Compartilhe ainda mais com o Famílias, ou receba segurança poderosa e confiável de senhas com o Equipes ou o Empresarial." + }, + "youHaveAGracePeriod": { + "message": "Você tem um período de tolerância de $DAYS$ dias até a data de expiração da sua assinatura. Resolva as faturas atrasadas até $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Gerenciar faturas" + }, + "yourNextChargeIsFor": { + "message": "Sua próxima cobrança é de" + }, + "dueOn": { + "message": "para" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Sua assinatura será suspensa em" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Sua assinatura foi suspensa em" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Sua assinatura será cancelada em" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Sua assinatura foi cancelada em" + }, + "storageFull": { + "message": "Armazenamento cheio" + }, + "storageUsedDescription": { + "message": "Você usou $USED$ dos $AVAILABLE$ GB do seu armazenamento de arquivos criptografados.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Você usou todos os $GB$ GB do seu armazenamento criptografado. Para continuar armazenando arquivos, adicione mais armazenamento." + }, + "whenYouRemoveStorage": { + "message": "Quando você remover o armazenamento, você receberá um crédito de conta proporcional que irá automaticamente para sua próxima fatura." + }, + "youHavePremium": { + "message": "Você tem o Premium" + }, + "emailProtected": { + "message": "E-mail protegido" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 929be5c7456..72ef9105d04 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Não existem membros para listar." }, + "noMembersToExport": { + "message": "Não existem membros para exportar." + }, "noEventsInList": { "message": "Não existem eventos para listar." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Ativado" }, + "optionEnabled": { + "message": "Ativado" + }, "restoreAccess": { "message": "Restaurar o acesso" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Chave" }, + "unnamedKey": { + "message": "Chave sem nome" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Código de verificação" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Para recuperar o acesso ao seu arquivo, reinicie a sua subscrição Premium. Se editar os detalhes de um item arquivado antes de reiniciar, ele será movido de volta para o seu cofre." }, + "itemRestored": { + "message": "O item foi restaurado" + }, "restartPremium": { "message": "Reiniciar Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send criado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send criado com sucesso!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ horas", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Novo Send de texto", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Novo Send de ficheiro", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send editado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revogado" }, + "accepted": { + "message": "Aceite" + }, "sendLink": { "message": "Link do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Inscrito na recuperação de conta" }, + "enrolled": { + "message": "Inscrito" + }, + "notEnrolled": { + "message": "Não inscrito" + }, "withdrawAccountRecovery": { "message": "Retirar-se da recuperação de conta" }, @@ -8833,7 +8885,7 @@ "message": "Ajuda na licença de funcionalidades pagas" }, "selfHostGracePeriodHelp": { - "message": "Após a expiração da sua subscrição, tem 60 dias para aplicar um ficheiro de licença atualizado à sua organização. O período de carência termina a $GRACE_PERIOD_END_DATE$.", + "message": "Após a expiração da sua subscrição, dispõe de 60 dias para aplicar um ficheiro de licença atualizado à sua organização. O período de tolerância termina a $GRACE_PERIOD_END_DATE$.", "placeholders": { "GRACE_PERIOD_END_DATE": { "content": "$1", @@ -10034,7 +10086,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "Dispõe de um período de carência de $DAYS$ dias a partir da data de expiração da sua subscrição para manter a sua subscrição. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", + "message": "Dispõe de um período de tolerância de $DAYS$ dias a partir da data de expiração da sua subscrição para manter a subscrição ativa. Por favor, resolva as faturas em atraso até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -10048,7 +10100,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "Dispõe de um período de carência de $DAYS$ dias a partir da data de vencimento da sua primeira fatura não paga para manter a sua subscrição. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", + "message": "Dispõe de um período de tolerância de $DAYS$ dias a partir da data de vencimento da sua primeira fatura em atraso para manter a subscrição ativa. Por favor, resolva as faturas em atraso até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -11462,7 +11514,7 @@ } }, "resellerPastDueWarningMsg": { - "message": "A fatura da sua subscrição não foi paga. Para garantir um serviço ininterrupto, contacte a $RESELLER$ para confirmar a sua renovação antes de $GRACE_PERIOD_END$.", + "message": "A fatura da sua subscrição não foi paga. Para garantir a continuidade do serviço, contacte a $RESELLER$ para confirmar a sua renovação antes de $GRACE_PERIOD_END$.", "placeholders": { "reseller": { "content": "$1", @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Esta credencial está em risco e não tem um site. Adicione um site e altere a palavra-passe para uma segurança mais forte." }, + "vulnerablePassword": { + "message": "Palavra-passe vulnerável." + }, + "changeNow": { + "message": "Alterar agora" + }, "missingWebsite": { "message": "Site em falta" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Desarquivar" }, + "archived": { + "message": "Arquivado" + }, "unArchiveAndSave": { "message": "Desarquivar e guardar" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Os itens foram movidos para o arquivo" }, + "itemWasUnarchived": { + "message": "O item foi desarquivado" + }, "itemUnarchived": { "message": "O item foi desarquivado" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Falha na verificação do utilizador." }, + "resizeSideNavigation": { + "message": "Redimensionar navegação lateral" + }, "recoveryDeleteCiphersTitle": { "message": "Eliminar itens irrecuperáveis do cofre" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Segurança total online" + }, + "updatePayment": { + "message": "Atualizar pagamento" + }, + "weCouldNotProcessYourPayment": { + "message": "Não foi possível processar o seu pagamento. Por favor, atualize o seu método de pagamento ou contacte a equipa de suporte para obter assistência." + }, + "yourSubscriptionHasExpired": { + "message": "A sua subscrição expirou. Por favor, contacte a equipa de suporte para obter assistência." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "A sua subscrição está agendada para ser cancelada em $DATE$. Pode reativá-la a qualquer momento até essa data.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Partilhe ainda mais com o plano Familiar ou obtenha uma segurança de palavras-passe poderosa e fiável com os planos Equipas ou Empresarial." + }, + "youHaveAGracePeriod": { + "message": "Dispõe de um período de tolerância de $DAYS$ dias a partir da data de expiração da sua subscrição. Por favor, resolva as faturas em atraso até $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Gerir faturas" + }, + "yourNextChargeIsFor": { + "message": "O seu próximo pagamento é de" + }, + "dueOn": { + "message": "com vencimento a" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "A sua subscrição será suspensa a" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "A sua subscrição foi suspensa a" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "A sua subscrição será cancelada a" + }, + "yourSubscriptionWasCanceledOn": { + "message": "A sua subscrição foi cancelada a" + }, + "storageFull": { + "message": "Armazenamento cheio" + }, + "storageUsedDescription": { + "message": "Utilizou $USED$ de $AVAILABLE$ GB do seu armazenamento de ficheiros encriptados.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Utilizou os $GB$ GB do seu armazenamento encriptado. Para continuar a guardar ficheiros, adicione mais espaço de armazenamento." + }, + "whenYouRemoveStorage": { + "message": "Ao remover espaço de armazenamento, receberá um crédito proporcional na conta, que será automaticamente aplicado na sua próxima fatura." + }, + "youHavePremium": { + "message": "Tem Premium" + }, + "emailProtected": { + "message": "E-mail protegido" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 80ee2d5e9b6..4511fb0a382 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Niciun eveniment de afișat." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Activat" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restaurare acces" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Cheie" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send salvat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send salvat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revocat" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Link Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index f82aa6d4417..292272706fe 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Нет участников для отображения." }, + "noMembersToExport": { + "message": "Нет участников для экспорта." + }, "noEventsInList": { "message": "Нет событий для отображения." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Включено" }, + "optionEnabled": { + "message": "Включено" + }, "restoreAccess": { "message": "Восстановить доступ" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ключ" }, + "unnamedKey": { + "message": "Безымянный ключ" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Код подтверждения" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Чтобы восстановить доступ к своему архиву, подключите подписку Премиум повторно. Если вы измените сведения об архивированном элементе перед переподключением, он будет перемещен обратно в ваше хранилище." }, + "itemRestored": { + "message": "Элемент восстановлен" + }, "restartPremium": { "message": "Переподключить Премиум" }, @@ -5604,6 +5616,37 @@ "message": "Send сохранена", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send успешно создана!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ час.", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Новая текстовая Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Новая файловая Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send сохранена", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Отозвано" }, + "accepted": { + "message": "Принято" + }, "sendLink": { "message": "Ссылка на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Зарегистрирован на восстановление аккаунта" }, + "enrolled": { + "message": "Зарегистрировано" + }, + "notEnrolled": { + "message": "Не зарегистрировано" + }, "withdrawAccountRecovery": { "message": "Сняться с восстановления аккаунта" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Этот логин подвержен риску и у него отсутствует веб-сайт. Добавьте веб-сайт и смените пароль для большей безопасности." }, + "vulnerablePassword": { + "message": "Уязвимый пароль." + }, + "changeNow": { + "message": "Изменить сейчас" + }, "missingWebsite": { "message": "Отсутствует сайт" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Разархивировать" }, + "archived": { + "message": "Архивирован" + }, "unArchiveAndSave": { "message": "Разархивировать и сохранить" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Элементы были отправлены в архив" }, + "itemWasUnarchived": { + "message": "Элемент был разархивирован" + }, "itemUnarchived": { "message": "Элемент был разархивирован" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Проверка пользователя не удалась." }, + "resizeSideNavigation": { + "message": "Изменить размер боковой навигации" + }, "recoveryDeleteCiphersTitle": { "message": "Удалить невосстановимые элементы хранилища" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Полная онлайн-защищенность" + }, + "updatePayment": { + "message": "Обновить платежную информацию" + }, + "weCouldNotProcessYourPayment": { + "message": "Нам не удалось обработать ваш платеж. Пожалуйста, обновите свой способ оплаты или обратитесь за помощью в службу поддержки." + }, + "yourSubscriptionHasExpired": { + "message": "Срок действия вашей подписки истек. Пожалуйста, обратитесь за помощью в службу поддержки." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Ваша подписка будет отменена $DATE$. Вы сможете восстановить ее в любое время до этого момента.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Делитесь еще большим количеством информации с семьями или обеспечьте надежную защиту паролем с командами или организациями." + }, + "youHaveAGracePeriod": { + "message": "У вас есть льготный период $DAYS$ дней с даты истечения срока действия вашей подписки. Пожалуйста, оплатите просроченные счета к $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Управление счетами" + }, + "yourNextChargeIsFor": { + "message": "Ваша следующая оплата будет за" + }, + "dueOn": { + "message": "срок" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Ваша подписка будет приостановлена в" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Ваша подписка была приостановлена" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Ваша подписка будет отменена в" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Ваша подписка была отменена" + }, + "storageFull": { + "message": "Хранилище заполнено" + }, + "storageUsedDescription": { + "message": "Вы использовали $USED$ из $AVAILABLE$ ГБ вашего зашифрованного файлового хранилища.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "Вы использовали все $GB$ вашего зашифрованного хранилища. Чтобы продолжить хранение файлов, добавьте дополнительное хранилище." + }, + "whenYouRemoveStorage": { + "message": "При удалении хранилища вы получите пропорциональную сумму на свой счет, которая автоматически пойдет на оплату вашего следующего счета." + }, + "youHavePremium": { + "message": "У вас Премиум" + }, + "emailProtected": { + "message": "Email защищен" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index e4166c07db2..16567585ce4 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index ea2d12bdb2c..ac2c3d8dd52 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Neexistujú žiadni členovia na zobrazenie." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Neexistujú žiadne udalosti na zobrazenie." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Povolené" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Obnoviť prístup" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Kľúč" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Overovací kód" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send vytvorený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send upravený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Zrušený prístup" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Odkaz na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Zapísaný na obnovu konta" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Odhlásiť sa z obnovy konta" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Toto prihlásenie je v ohrození a chýba mu webová stránka. Pridajte webovú stránku a zmeňte heslo na silnejšie zabezpečenie." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Chýbajúca webová stránka" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Zrušiť archiváciu" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Položky boli archivované" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Položka bola odobraná z archívu" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Zlyhalo overenie používateľa." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 1f880d87412..a8f1f22037e 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Ni članov za prikaz." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Ni dogodkov za prikaz." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Omogočeno" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Obnovi dostop" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ključ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Pošiljka shranjena", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Pošiljka shranjena", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Povezava pošiljke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 3ebf0057bb9..6d212a60150 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Omogućeno" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ključ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Napravljena slanja", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Izmenjena slanja", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index b7934ec62fb..0e82f68f820 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Нема чланова за приказивање." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Нема догађаја у листи." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Омогућено" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Врати притуп" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Кључ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Верификациони кôд" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Креирај „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "„Send“ уређено", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Опозвано" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "УРЛ „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Уписан/а у опоравак налога" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Повуците са опоравка налога" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Ова пријава је ризична и недостаје веб локација. Додајте веб страницу и промените лозинку за јачу сигурност." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Недостаје веб страница" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Врати из архиве" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Ставке су послате у архиву" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Ставка враћена из архиве" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index c428b1561ed..1f86e509b3c 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Det finns inga medlemmar att visa." }, + "noMembersToExport": { + "message": "Det finns inga medlemmar att exportera." + }, "noEventsInList": { "message": "Det finns inga händelser att visa." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Aktiverad" }, + "optionEnabled": { + "message": "Aktiverat" + }, "restoreAccess": { "message": "Återställ åtkomst" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Nyckel" }, + "unnamedKey": { + "message": "Namnlös nyckel" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verifieringskod" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "För att återfå åtkomst till ditt arkiv, starta om Premium-prenumerationen. Om du redigerar detaljer för ett arkiverat objekt innan du startar om kommer det att flyttas tillbaka till ditt valv." }, + "itemRestored": { + "message": "Objektet har återställts" + }, "restartPremium": { "message": "Starta om Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send har sparats", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send skapades!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ timmar", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Ny Send för text", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Ny Send för fil", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send har sparats", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Återkallad" }, + "accepted": { + "message": "Accepterade" + }, "sendLink": { "message": "Send-länk", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Registrerad i kontoåterställning" }, + "enrolled": { + "message": "Registrerade" + }, + "notEnrolled": { + "message": "Inte registrerade" + }, "withdrawAccountRecovery": { "message": "Uttag från kontoåterställning" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Denna inloggning är i riskzonen och saknar en webbplats. Lägg till en webbplats och ändra lösenordet för starkare säkerhet." }, + "vulnerablePassword": { + "message": "Sårbart lösenord." + }, + "changeNow": { + "message": "Ändra nu" + }, "missingWebsite": { "message": "Saknar webbplats" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Avarkivera" }, + "archived": { + "message": "Arkiverade" + }, "unArchiveAndSave": { "message": "Avarkivera och spara" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Objekten har skickats till arkivet" }, + "itemWasUnarchived": { + "message": "Objektet har avarkiverats" + }, "itemUnarchived": { "message": "Objektet har avarkiverats" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Verifiering av användare misslyckades." }, + "resizeSideNavigation": { + "message": "Ändra storlek på sidnavigering" + }, "recoveryDeleteCiphersTitle": { "message": "Ta bort oåterkalleliga valvobjekt" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Komplett säkerhet online" + }, + "updatePayment": { + "message": "Uppdatera betalning" + }, + "weCouldNotProcessYourPayment": { + "message": "Vi kunde inte behandla din betalning. Vänligen uppdatera din betalningsmetod eller kontakta supportteamet för hjälp." + }, + "yourSubscriptionHasExpired": { + "message": "Ditt abonnemang har löpt ut. Kontakta supportteamet för hjälp." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Ditt abonnemang är planerat att avslutas den $DATE$. Du kan återställa det när som helst innan dess.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Dela ännu mer med Familjer eller få kraftfull, betrodd lösenordssäkerhet med Team eller Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Hantera fakturor" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Lagringen är full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index 5ad16f4e27e..227ac6ae29e 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "பட்டியலிட உறுப்பினர்கள் யாரும் இல்லை." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "பட்டியலிட நிகழ்வுகள் எதுவும் இல்லை." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "இயக்கப்பட்டது" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "அணுகலை மீட்டமை" }, @@ -2634,6 +2640,9 @@ "key": { "message": "கீ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "சரிபார்ப்புக் குறியீடு" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send சேமிக்கப்பட்டது", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send சேமிக்கப்பட்டது", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "திரும்பப் பெறப்பட்டது" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send இணைப்பு", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "கணக்கு மீட்டெடுப்பில் பதிவுசெய்யப்பட்டது" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "கணக்கு மீட்டெடுப்பிலிருந்து விலகு" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index f570d355369..b352e56ba4f 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "There are no events to list." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 8efc54bf052..f444fe29ae4 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "There are no members to list." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "ไม่มีเหตุการณ์สำหรับแสดง" }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Turned on" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Restore access" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Key" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Verification code" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Revoked" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Enrolled in account recovery" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Withdraw from account recovery" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 27e1740fca7..28d11d24f1b 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Listelenecek üye yok." }, + "noMembersToExport": { + "message": "Dışa aktarılacak üye yok." + }, "noEventsInList": { "message": "Listelenecek olay yok." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Etkinleştirildi" }, + "optionEnabled": { + "message": "Etkinleştirildi" + }, "restoreAccess": { "message": "Erişimi geri getir" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Anahtar" }, + "unnamedKey": { + "message": "Adsız anahtar" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Doğrulama kodu" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Arşivinize yeniden erişim kazanmak için Premium aboneliğinizi yeniden başlatın. Yeniden başlatmadan önce arşivlenmiş bir kaydın ayrıntılarını düzenlerseniz, kayıt tekrar kasanıza taşınır." }, + "itemRestored": { + "message": "Kayıt geri yüklendi" + }, "restartPremium": { "message": "Premium’u yeniden başlat" }, @@ -5604,6 +5616,37 @@ "message": "Send kaydedildi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send başarıyla oluşturuldu.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ saat", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "Yeni Send metni", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "Yeni Send dosyası", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send kaydedildi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "İptal edildi" }, + "accepted": { + "message": "Kabul edildi" + }, "sendLink": { "message": "Send bağlantısı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Hesap kurtarmaya kaydolundu" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Hesap kurtarmadan ayrıl" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Bu hesap risk altında ve web sitesi eksik. Bir web sitesi ekleyin ve güvenliğinizi artırmak için parolayı değiştirin." }, + "vulnerablePassword": { + "message": "Güvensiz parola." + }, + "changeNow": { + "message": "Şimdi değiştir" + }, "missingWebsite": { "message": "Web sitesi eksik" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Arşivden çıkar" }, + "archived": { + "message": "Arşivlendi" + }, "unArchiveAndSave": { "message": "Arşivden çıkar ve kaydet" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Kayıtlar arşive gönderildi" }, + "itemWasUnarchived": { + "message": "Kayıt arşivden çıkarıldı" + }, "itemUnarchived": { "message": "Kayıt arşivden çıkarıldı" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Kullanıcı doğrulaması başarısız oldu." }, + "resizeSideNavigation": { + "message": "Kenar menüsünü yeniden boyutlandır" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Eksiksiz çevrimiçi güvenlik" + }, + "updatePayment": { + "message": "Ödemeyi güncelle" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Faturaları yönet" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Depolama alanı dolu" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "Premium abonesiniz" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index f604d38059f..5de4cf2b8bd 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Список не містить учасників." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Немає подій." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Увімкнено" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Відновити доступ" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Ключ" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Код підтвердження" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "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." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Restart Premium" }, @@ -5604,6 +5616,37 @@ "message": "Відправлення збережено", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Відправлення збережено", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Відкликані" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Посилання на відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Розгорнуто на відновлення облікового запису" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Відкликати відновлення облікового запису" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "This login is at-risk and missing a website. Add a website and change the password for stronger security." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Missing website" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Unarchive" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Items were sent to archive" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Item was unarchived" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "User verification failed." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Delete unrecoverable vault items" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 1fb9e911225..a2498cea4cb 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "Không có người nào để liệt kê." }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "Chưa có sự kiện nào." }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "Kích hoạt" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "Khôi phục quyền truy cập" }, @@ -2634,6 +2640,9 @@ "key": { "message": "Khóa" }, + "unnamedKey": { + "message": "Unnamed key" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "Mã xác minh" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "Để lấy lại quyền truy cập vào lưu trữ của bạn, hãy khởi động lại gói đăng ký Cao cấp. Nếu bạn chỉnh sửa chi tiết cho một mục đã lưu trữ trước khi khởi động lại, mục đó sẽ được chuyển trở lại kho của bạn." }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "Khởi động lại gói Cao cấp" }, @@ -5604,6 +5616,37 @@ "message": "Đã lưu Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Đã lưu Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "Đã thu hồi" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Liên kết Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "Đã đăng ký khôi phục tài khoản" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "Rút khỏi khôi phục tài khoản" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "Mục đăng nhập này có nguy cơ bị lộ và thiếu trang web. Thêm trang web và thay đổi mật khẩu để tăng cường bảo mật." }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "Thiếu trang web" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "Hủy lưu trữ" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "Unarchive and save" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "Các mục đã được chuyển vào lưu trữ" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "Mục đã được bỏ lưu trữ" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "Xác minh người dùng thất bại." }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "Xóa các mục kho không thể khôi phục" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "Complete online security" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index b41635b948c..a87f850aa31 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -42,7 +42,7 @@ "message": "您还没有创建报告" }, "notifiedMembers": { - "message": "已通知的成员" + "message": "已通知成员" }, "revokeMembers": { "message": "撤销成员" @@ -170,7 +170,7 @@ } }, "notifiedMembersWithCount": { - "message": "已通知的成员 ($COUNT$)", + "message": "已通知成员 ($COUNT$)", "placeholders": { "count": { "content": "$1", @@ -236,7 +236,7 @@ "message": "标记为关键的应用程序" }, "criticalApplicationsMarkedSuccess": { - "message": "$COUNT$ 个标记为关键的应用程序", + "message": "$COUNT$ 个应用程序标记为关键", "placeholders": { "count": { "content": "$1", @@ -341,7 +341,7 @@ "message": "总的应用程序" }, "applicationsNeedingReview": { - "message": "应用程序需要审查" + "message": "需要审查的应用程序" }, "newApplicationsCardTitle": { "message": "审查新应用程序" @@ -365,7 +365,7 @@ "message": "全部处理完成!" }, "noNewApplicationsToReviewAtThisTime": { - "message": "目前没有新应用程序需要审查" + "message": "当前没有新应用程序需要审查" }, "organizationHasItemsSavedForApplications": { "message": "您的组织已为 $COUNT$ 个应用程序保存了项目", @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "没有可列出的成员。" }, + "noMembersToExport": { + "message": "没有可导出的成员。" + }, "noEventsInList": { "message": "没有可列出的事件。" }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "已启用" }, + "optionEnabled": { + "message": "已启用" + }, "restoreAccess": { "message": "恢复访问权限" }, @@ -2634,6 +2640,9 @@ "key": { "message": "密钥" }, + "unnamedKey": { + "message": "未命名的密钥" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "验证码" }, @@ -3141,7 +3150,10 @@ "message": "您的高级版订阅已结束" }, "premiumSubscriptionEndedDesc": { - "message": "要重新获取存档的访问权限,请重启您的高级版订阅。如果您在重启前编辑了存档项目的详细信息,它将被移回您的密码库中。" + "message": "要重新获取归档内容的访问权限,请重启您的高级版订阅。如果您在重启前编辑了某个已归档项目的详细信息,它将被移回您的密码库中。" + }, + "itemRestored": { + "message": "项目已恢复" }, "restartPremium": { "message": "重启高级版" @@ -3213,7 +3225,7 @@ "message": "您的方案包含了 7 天的免费试用。在试用期结束前,不会从您的付款方式中扣款。您可以随时取消。" }, "paymentInformation": { - "message": "支付信息" + "message": "付款信息" }, "billingInformation": { "message": "计费信息" @@ -3222,7 +3234,7 @@ "message": "在 7 天免费试用期间,不会从您的付款方式中扣款。" }, "creditCard": { - "message": "支付卡" + "message": "信用卡" }, "paypalClickSubmit": { "message": "选择 PayPal 按钮登录您的 PayPal 账户,然后点击下面的「提交」按钮继续。" @@ -3671,7 +3683,7 @@ "message": "获取帮助" }, "getApps": { - "message": "获取应用" + "message": "获取 App" }, "loggedInAs": { "message": "已登录为" @@ -3942,7 +3954,7 @@ } }, "deletedItemId": { - "message": "发送项目 $ID$ 到回收站。", + "message": "发送了项目 $ID$ 到回收站。", "placeholders": { "id": { "content": "$1", @@ -4002,7 +4014,7 @@ } }, "viewedSecurityCodeItemId": { - "message": "查看了项目 $ID$ 的安全代码。", + "message": "查看了项目 $ID$ 的安全码。", "placeholders": { "id": { "content": "$1", @@ -4047,7 +4059,7 @@ } }, "copiedSecurityCodeItemId": { - "message": "复制了项目 $ID$ 的安全代码。", + "message": "复制了项目 $ID$ 的安全码。", "placeholders": { "id": { "content": "$1", @@ -5229,7 +5241,7 @@ "message": "您的 API 密钥可用于验证 Bitwarden 公共 API。" }, "apiKeyRotateDesc": { - "message": "轮换 API 密钥将使前一个密钥失效。如果您认为当前密钥不再安全,可以轮换 API 密钥。" + "message": "轮换 API 密钥将使之前的密钥失效。如果您认为当前密钥不再安全,您可以轮换您的 API 密钥。" }, "apiKeyWarning": { "message": "您的 API 密钥拥有组织的全部访问权限。请严格保密。" @@ -5413,7 +5425,7 @@ "message": "项目已恢复" }, "restoredItemId": { - "message": "项目 $ID$ 已恢复", + "message": "恢复了项目 $ID$", "placeholders": { "id": { "content": "$1", @@ -5422,7 +5434,7 @@ } }, "vaultTimeoutLogOutConfirmation": { - "message": "超时后注销将解除对密码库的所有访问权限,并需要进行在线身份验证。确定使用此设置吗?" + "message": "超时后注销账户将解除对密码库的所有访问权限,并需要进行在线身份验证。确定要使用此设置吗?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "超时动作确认" @@ -5559,7 +5571,7 @@ "message": "组织的所有者和管理员不受此策略的约束。" }, "limitSendViews": { - "message": "查看次数限制" + "message": "限制查看次数" }, "limitSendViewsHint": { "message": "达到限额后,任何人无法查看此 Send。", @@ -5604,6 +5616,37 @@ "message": "Send 已保存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send 创建成功!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ 小时", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "新增文本 Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "新增文件 Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 已保存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "已撤销" }, + "accepted": { + "message": "已接受" + }, "sendLink": { "message": "Send 链接", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5692,7 +5738,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { - "message": "此 Send 默认隐藏。您可使用下方的按钮切换其可见性。", + "message": "此 Send 默认隐藏。您可以使用下方的按钮切换其可见性。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "downloadAttachments": { @@ -5935,7 +5981,7 @@ "message": "自动用户确认可能对您的组织数据带来安全风险。" }, "autoConfirmAcceptSecurityRiskLearnMore": { - "message": "进一步了解此风险", + "message": "了解此风险", "description": "The is the link copy for the first check box option in the edit policy dialog" }, "autoConfirmSingleOrgRequired": { @@ -6118,7 +6164,7 @@ "message": "组织策略已阻止将项目导入您的个人密码库。" }, "personalOwnershipCheckboxDesc": { - "message": "移除组织用户的个人所有权" + "message": "禁用组织用户的个人所有权" }, "send": { "message": "Send", @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "已注册账户恢复" }, + "enrolled": { + "message": "已注册" + }, + "notEnrolled": { + "message": "未注册" + }, "withdrawAccountRecovery": { "message": "撤销账户恢复" }, @@ -6359,7 +6411,7 @@ "message": "重置密码" }, "resetPasswordLoggedOutWarning": { - "message": "继续操作会将 $NAME$ 登出当前会话,要求他们重新登录。在其他设备上的活动会话可能继续活动长达一个小时。", + "message": "继续操作会将 $NAME$ 登出当前会话,并要求他们重新登录。在其他设备上的活动会话可能继续活动长达一个小时。", "placeholders": { "name": { "content": "$1", @@ -6368,7 +6420,7 @@ } }, "emergencyAccessLoggedOutWarning": { - "message": "继续操作会将 $NAME$ 登出当前会话,要求他们重新登录。在其他设备上的活动会话可能继续活动长达一个小时。", + "message": "继续操作会将 $NAME$ 登出当前会话,并要求他们重新登录。在其他设备上的活动会话可能继续活动长达一个小时。", "placeholders": { "name": { "content": "$1", @@ -6790,7 +6842,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为 $ACTION$。", + "message": "您的组织策略正在影响您的密码库超时。最大允许的密码库超时为 $HOURS$ 小时 $MINUTES$ 分钟。您的密码库超时动作被设置为「$ACTION$」。", "placeholders": { "hours": { "content": "$1", @@ -8878,7 +8930,7 @@ "message": "群组/用户" }, "kdfSettingsChangeLogoutWarning": { - "message": "接下来将会注销您所有的活动会话。您需要重新登录并完成两步登录(如果有)。我们建议您在更改加密设置前导出密码库,以防止数据丢失。" + "message": "继续操作将会注销您所有的活动会话。您需要重新登录并完成两步登录(如果有)。我们建议您在更改加密设置前导出密码库,以防止数据丢失。" }, "secretsManager": { "message": "机密管理器" @@ -10545,7 +10597,7 @@ "description": "Button text to navigate back" }, "removeItem": { - "message": "删除 $NAME$", + "message": "移除 $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -10846,7 +10898,7 @@ "message": "进一步了解搜索密码库" }, "learnMoreAboutYourAccountFingerprintPhrase": { - "message": "进一步了解账户指纹短语" + "message": "了解您的账户指纹短语" }, "impactOfRotatingYourEncryptionKey": { "message": "轮换加密密钥的影响" @@ -11170,7 +11222,7 @@ } }, "organizationUserDeletedDesc": { - "message": "该用户已从组织中删除,所有关联的用户数据也已删除。" + "message": "该用户已从组织中移除,所有关联的用户数据也已被删除。" }, "deletedUserIdEventMessage": { "message": "删除了用户 $ID$", @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登录存在风险且缺少网站。请添加网站并更改密码以增强安全性。" }, + "vulnerablePassword": { + "message": "易受攻击的密码。" + }, + "changeNow": { + "message": "立即更改" + }, "missingWebsite": { "message": "缺少网站" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "取消归档" }, + "archived": { + "message": "已归档" + }, "unArchiveAndSave": { "message": "取消归档并保存" }, @@ -11624,14 +11685,17 @@ "itemsWereSentToArchive": { "message": "项目已发送到归档" }, + "itemWasUnarchived": { + "message": "项目已取消归档" + }, "itemUnarchived": { "message": "项目已取消归档" }, "bulkArchiveItems": { - "message": "项目已归档" + "message": "已归档的项目" }, "bulkUnarchiveItems": { - "message": "项目已取消归档" + "message": "已取消归档的项目" }, "archiveItem": { "message": "归档项目", @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "用户验证失败。" }, + "resizeSideNavigation": { + "message": "调整侧边导航栏大小" + }, "recoveryDeleteCiphersTitle": { "message": "删除无法恢复的密码库项目" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "全面的在线安全防护" + }, + "updatePayment": { + "message": "更新付款信息" + }, + "weCouldNotProcessYourPayment": { + "message": "我们无法处理您的付款。请更新您的付款方式或联系支持团队寻求帮助。" + }, + "yourSubscriptionHasExpired": { + "message": "您的订阅已过期。请联系支持团队寻求帮助。" + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "您的订阅将于 $DATE$ 取消。在此日期之前,您可以随时恢复订阅。", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "使用家庭版共享更多内容,或使用团队版或企业版获得强大、可信赖的密码安全防护。" + }, + "youHaveAGracePeriod": { + "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期。请在 $DATE$ 之前处理逾期未支付的账单。", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "管理账单" + }, + "yourNextChargeIsFor": { + "message": "您的下一次收费是用于" + }, + "dueOn": { + "message": "到期日期为" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "您的订阅将被暂停于" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "您的订阅被暂停于" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "您的订阅将被取消于" + }, + "yourSubscriptionWasCanceledOn": { + "message": "您的订阅被取消于" + }, + "storageFull": { + "message": "存储空间已满" + }, + "storageUsedDescription": { + "message": "总计 $AVAILABLE$ GB 加密文件存储空间,您已使用 $USED$ GB。", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "您已使用了全部的 $GB$ GB 加密存储空间。要继续存储文件,请添加更多存储空间。" + }, + "whenYouRemoveStorage": { + "message": "当您移除存储空间时,您将收到一笔按比例计算的账户信用额度,其将用于自动抵扣您的下一笔费用。" + }, + "youHavePremium": { + "message": "您拥有高级版" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 4fbf08c28a7..5fe77ca13c6 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1749,6 +1749,9 @@ "noMembersInList": { "message": "沒有可列出的成員。" }, + "noMembersToExport": { + "message": "There are no members to export." + }, "noEventsInList": { "message": "沒有可列出的事件。" }, @@ -2537,6 +2540,9 @@ "enabled": { "message": "已啟用" }, + "optionEnabled": { + "message": "Enabled" + }, "restoreAccess": { "message": "還原存取權限" }, @@ -2634,6 +2640,9 @@ "key": { "message": "金鑰" }, + "unnamedKey": { + "message": "未命名金鑰" + }, "twoStepAuthenticatorEnterCodeV2": { "message": "驗證碼" }, @@ -3143,6 +3152,9 @@ "premiumSubscriptionEndedDesc": { "message": "若要重新存取您的封存項目,請重新啟用進階版訂閱。若您在重新啟用前編輯封存項目的詳細資料,它將會被移回您的密碼庫。" }, + "itemRestored": { + "message": "Item has been restored" + }, "restartPremium": { "message": "重新啟用進階版" }, @@ -5604,6 +5616,37 @@ "message": "Send 已儲存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendCreatedSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendCreatedDescription": { + "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" + } + } + }, + "durationTimeHours": { + "message": "$HOURS$ hours", + "placeholders": { + "hours": { + "content": "$1", + "example": "5" + } + } + }, + "newTextSend": { + "message": "New Text Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "newFileSend": { + "message": "New File Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 已儲存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -5643,6 +5686,9 @@ "revoked": { "message": "已撤銷" }, + "accepted": { + "message": "Accepted" + }, "sendLink": { "message": "Send 連結", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -6301,6 +6347,12 @@ "enrolledAccountRecovery": { "message": "已注冊帳戶復原" }, + "enrolled": { + "message": "Enrolled" + }, + "notEnrolled": { + "message": "Not enrolled" + }, "withdrawAccountRecovery": { "message": "撤銷帳戶復原" }, @@ -11568,6 +11620,12 @@ "changeAtRiskPasswordAndAddWebsite": { "message": "此登入資訊存在風險,且缺少網站。請新增網站並變更密碼以提升安全性。" }, + "vulnerablePassword": { + "message": "Vulnerable password." + }, + "changeNow": { + "message": "Change now" + }, "missingWebsite": { "message": "缺少網站" }, @@ -11606,6 +11664,9 @@ "unArchive": { "message": "取消封存" }, + "archived": { + "message": "Archived" + }, "unArchiveAndSave": { "message": "取消封存並儲存" }, @@ -11624,6 +11685,9 @@ "itemsWereSentToArchive": { "message": "項目已移至封存" }, + "itemWasUnarchived": { + "message": "Item was unarchived" + }, "itemUnarchived": { "message": "項目取消封存" }, @@ -12298,6 +12362,9 @@ "userVerificationFailed": { "message": "使用者驗證失敗。" }, + "resizeSideNavigation": { + "message": "Resize side navigation" + }, "recoveryDeleteCiphersTitle": { "message": "刪除無法復原的密碼庫項目" }, @@ -12479,5 +12546,88 @@ }, "planDescPremium": { "message": "完整的線上安全防護" + }, + "updatePayment": { + "message": "Update payment" + }, + "weCouldNotProcessYourPayment": { + "message": "We could not process your payment. Please update your payment method or contact the support team for assistance." + }, + "yourSubscriptionHasExpired": { + "message": "Your subscription has expired. Please contact the support team for assistance." + }, + "yourSubscriptionIsScheduledToCancel": { + "message": "Your subscription is scheduled to cancel on $DATE$. You can reinstate it anytime before then.", + "placeholders": { + "date": { + "content": "$1", + "example": "Dec. 22, 2025" + } + } + }, + "premiumShareEvenMore": { + "message": "Share even more with Families, or get powerful, trusted password security with Teams or Enterprise." + }, + "youHaveAGracePeriod": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date. Please resolve the past due invoices by $DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "14" + }, + "date": { + "content": "$2", + "example": "Dec. 22, 2025" + } + } + }, + "manageInvoices": { + "message": "Manage invoices" + }, + "yourNextChargeIsFor": { + "message": "Your next charge is for" + }, + "dueOn": { + "message": "due on" + }, + "yourSubscriptionWillBeSuspendedOn": { + "message": "Your subscription will be suspended on" + }, + "yourSubscriptionWasSuspendedOn": { + "message": "Your subscription was suspended on" + }, + "yourSubscriptionWillBeCanceledOn": { + "message": "Your subscription will be canceled on" + }, + "yourSubscriptionWasCanceledOn": { + "message": "Your subscription was canceled on" + }, + "storageFull": { + "message": "Storage full" + }, + "storageUsedDescription": { + "message": "You have used $USED$ out of $AVAILABLE$ GB of your encrypted file storage.", + "placeholders": { + "used": { + "content": "$1", + "example": "1" + }, + "available": { + "content": "$2", + "example": "5" + } + } + }, + "storageFullDescription": { + "message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage." + }, + "whenYouRemoveStorage": { + "message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill." + }, + "youHavePremium": { + "message": "You have Premium" + }, + "emailProtected": { + "message": "Email protected" } } diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/README.md b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/README.md new file mode 100644 index 00000000000..1796b0db071 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/README.md @@ -0,0 +1,358 @@ +# Adding a New Integration Configuration and Template + +This guide explains how to add a new integration type (e.g., Datadog, Splunk HEC) to the organization integrations system. + +## Step 1: Define the Configuration Class + +Create a new configuration class that implements `OrgIntegrationConfiguration`: + +```typescript +export class MyServiceConfiguration implements OrgIntegrationConfiguration { + // Required: Specify which service this configuration is for + bw_serviceName: OrganizationIntegrationServiceName; + + // Add service-specific properties (e.g., uri, apiKey, token) + uri: string; + apiKey: string; + + constructor(uri: string, apiKey: string, bw_serviceName: OrganizationIntegrationServiceName) { + this.uri = uri; + this.apiKey = apiKey; + this.bw_serviceName = bw_serviceName; + } + + // Required: Serialize configuration to JSON string for API transmission + // Property names should match PascalCase for backend compatibility + // Example: "Uri", "ApiKey" - the backend expects PascalCase keys + toString(): string { + return JSON.stringify({ + Uri: this.uri, + ApiKey: this.apiKey, + bw_serviceName: this.bw_serviceName, + }); + } +} +``` + +**Required Interface Properties:** + +- `bw_serviceName: OrganizationIntegrationServiceName` - Identifies the external service +- `toString(): string` - Serializes configuration for API storage + +## Step 2: Define the Template Class + +Create a template class that implements `OrgIntegrationTemplate`: + +```typescript +export class MyServiceTemplate implements OrgIntegrationTemplate { + // Required: Specify which service this template is for + bw_serviceName: OrganizationIntegrationServiceName; + + // Add template-specific properties with placeholders (e.g., #CipherId#, #UserEmail#) + // These placeholders will be replaced with actual values at runtime + + constructor(service: OrganizationIntegrationServiceName) { + this.bw_serviceName = service; + } + + // Required: Serialize template to JSON string + // Define the structure of data that will be sent to the external service + toString(): string { + return JSON.stringify({ + bw_serviceName: this.bw_serviceName, + event: { + type: "#Type#", + userId: "#UserId#", + // ... other placeholders + }, + }); + } +} +``` + +**Required Interface Properties:** + +- `bw_serviceName: OrganizationIntegrationServiceName` - Identifies the external service +- `toString(): string` - Serializes template structure with placeholders + +## Step 3: Update OrganizationIntegrationType + +Add your new integration type to the enum: + +```typescript +export const OrganizationIntegrationType = Object.freeze({ + // ... existing types + MyService: 7, +} as const); +``` + +## Step 4: Extend OrgIntegrationBuilder + +The `OrgIntegrationBuilder` is the central factory for creating and deserializing integration configurations and templates. +It provides a consistent API for the `OrganizationIntegrationService` to work with different integration types. + +Add four methods to `OrgIntegrationBuilder`: + +### 4a. Add a static factory method for configuration: + +```typescript +static buildMyServiceConfiguration( + uri: string, + apiKey: string, + bw_serviceName: OrganizationIntegrationServiceName +): OrgIntegrationConfiguration { + return new MyServiceConfiguration(uri, apiKey, bw_serviceName); +} +``` + +### 4b. Add a static factory method for template: + +```typescript +static buildMyServiceTemplate( + bw_serviceName: OrganizationIntegrationServiceName +): OrgIntegrationTemplate { + return new MyServiceTemplate(bw_serviceName); +} +``` + +### 4c. Add a case to `buildConfiguration()` switch statement: + +```typescript +case OrganizationIntegrationType.MyService: { + const config = this.convertToJson(configuration); + return this.buildMyServiceConfiguration(config.uri, config.apiKey, config.bw_serviceName); +} +``` + +This allows deserialization of JSON configuration strings from the API into typed objects. + +### 4d. Add a case to `buildTemplate()` switch statement: + +```typescript +case OrganizationIntegrationType.MyService: { + const template = this.convertToJson(template); + return this.buildMyServiceTemplate(template.bw_serviceName); +} +``` + +This allows deserialization of JSON template strings from the API into typed objects. + +## How This Facilitates OrganizationIntegrationService + +The `OrgIntegrationBuilder` acts as an abstraction layer that enables the `OrganizationIntegrationService` to: + +1. **Save/Update Operations**: Accept strongly-typed configuration and template objects, serialize them via `toString()`, + and send to the API as JSON strings. + +2. **Load Operations**: Receive JSON strings from the API, use `buildConfiguration()` and `buildTemplate()` to + deserialize them into strongly-typed objects through the builder's factory methods. + +3. **Type Safety**: Work with typed domain models (`OrgIntegrationConfiguration`, `OrgIntegrationTemplate`) without + knowing the specific implementation details of each integration type. + +4. **Extensibility**: Add new integration types without modifying the service layer logic. The service only needs to + call the builder's methods, which internally route to the correct implementation based on `OrganizationIntegrationType`. + +5. **Property Normalization**: The builder's `normalizePropertyCase()` method handles conversion between PascalCase + (backend) and camelCase (frontend), ensuring seamless data flow regardless of API naming conventions. + +The service uses these capabilities in methods like `save()`, `update()`, and `mapResponsesToOrganizationIntegration()` +to manage the complete lifecycle of integration configurations and templates. + +## Step 5: Add Service Name to OrganizationIntegrationServiceName + +If you're adding a new external service (not just a new integration type for an existing service), +add it to the `OrganizationIntegrationServiceName` enum in `organization-integration-service-type.ts`: + +```typescript +export const OrganizationIntegrationServiceName = Object.freeze({ + CrowdStrike: "CrowdStrike", + Datadog: "Datadog", + MyService: "MyService", // Add your new service +} as const); +``` + +This identifies the external service your integration connects to. The `bw_serviceName` property in your +configuration and template classes should use a value from this enum. + +## Step 6: File Organization + +Place your new files in the following directories: + +- **Configuration classes**: `models/configuration/` + - Example: `models/configuration/myservice-configuration.ts` +- **Template classes**: `models/integration-configuration-config/configuration-template/` + - Example: `models/integration-configuration-config/configuration-template/myservice-template.ts` + +This organization keeps related files grouped and maintains consistency with existing integrations. + +## Important Conventions + +### Template Placeholders + +Templates support standardized placeholders that are replaced with actual values at runtime. +Use the following format with hashtags: + +**Common placeholders**: + +- `#EventMessage#` - Full event message +- `#Type#` - Event type +- `#CipherId#` - Cipher/item identifier +- `#CollectionId#` - Collection identifier +- `#GroupId#` - Group identifier +- `#PolicyId#` - Policy identifier +- `#UserId#` - User identifier +- `#ActingUserId#` - User performing the action +- `#UserName#` - User's name +- `#UserEmail#` - User's email +- `#ActingUserName#` - Acting user's name +- `#ActingUserEmail#` - Acting user's email +- `#DateIso8601#` - ISO 8601 formatted date +- `#DeviceType#` - Device type +- `#IpAddress#` - IP address +- `#SecretId#` - Secret identifier +- `#ProjectId#` - Project identifier +- `#ServiceAccountId#` - Service account identifier + +These placeholders are processed server-side when events are sent to the external service. +**_Also, these placeholders are determined by the server-side implementation, so ensure your template matches the expected format._** + +## Step 7: Add Tests + +Add comprehensive tests for your new integration in three test files: + +### 7a. Integration Service Tests + +Add tests in `organization-integration-service.spec.ts`: + +```typescript +describe("MyService integration", () => { + it("should save a new MyService integration successfully", async () => { + const config = OrgIntegrationBuilder.buildMyServiceConfiguration( + "https://test.myservice.com", + "test-api-key", + OrganizationIntegrationServiceName.MyService, + ); + const template = OrgIntegrationBuilder.buildMyServiceTemplate( + OrganizationIntegrationServiceName.MyService, + ); + // ... test implementation + }); +}); +``` + +The implementation should cover save, update, delete, and load operations. +This is all that is required to make a new integration type functional within the service. + +--- + +## Understanding the Architecture + +**Workflow**: + +1. Call `setOrganizationId(orgId)` to load integrations for an organization +2. Subscribe to `integrations$` to receive the loaded integrations +3. Any save/update/delete operations automatically update `integrations$` + +The service uses `BehaviorSubject` internally to manage state and emit updates to all subscribers. + +### Error Handling Pattern + +All modification operations (`save()`, `update()`, `delete()`) return `IntegrationModificationResult`: + +```typescript +type IntegrationModificationResult = { + success: boolean; // Operation succeeded + mustBeOwner: boolean; // If false, permission denied (404) - user must be organization owner +}; +``` + +This pattern allows the UI to provide specific feedback when users lack necessary permissions. + +### Configuration vs Template + +Understanding the distinction between these two concepts is crucial: + +**Configuration (`OrgIntegrationConfiguration`)**: + +- Contains authentication and connection details +- Example: API URLs, tokens, API keys, authentication schemes +- Stored in the `Integration` record +- Usually contains sensitive data +- One per integration + +**Template (`OrgIntegrationTemplate`)**: + +- Defines the structure and format of event data +- Contains placeholders like `#UserId#`, `#EventMessage#` +- Stored in the `IntegrationConfiguration` record +- No sensitive data +- Specifies how Bitwarden events map to external service format +- One per integration (current implementation) + +When an event occurs, the system: + +1. Uses the **Configuration** to know where and how to send data +2. Uses the **Template** to format the event data for that specific service + +## Example: Complete Integration + +Here's a minimal example showing all pieces working together: + +```typescript +// 1. Configuration +export class ExampleConfiguration implements OrgIntegrationConfiguration { + uri: string; + apiKey: string; + bw_serviceName: OrganizationIntegrationServiceName; + + constructor(uri: string, apiKey: string, bw_serviceName: OrganizationIntegrationServiceName) { + this.uri = uri; + this.apiKey = apiKey; + this.bw_serviceName = bw_serviceName; + } + + toString(): string { + return JSON.stringify({ + Uri: this.uri, + ApiKey: this.apiKey, + bw_serviceName: this.bw_serviceName, + }); + } +} + +// 2. Template +export class ExampleTemplate implements OrgIntegrationTemplate { + bw_serviceName: OrganizationIntegrationServiceName; + + constructor(bw_serviceName: OrganizationIntegrationServiceName) { + this.bw_serviceName = bw_serviceName; + } + + toString(): string { + return JSON.stringify({ + bw_serviceName: this.bw_serviceName, + event: { + type: "#Type#", + user: "#UserEmail#", + timestamp: "#DateIso8601#", + }, + }); + } +} + +// 3. Usage in OrganizationIntegrationService +const config = OrgIntegrationBuilder.buildExampleConfiguration( + "https://api.example.com", + "secret-key", + OrganizationIntegrationServiceName.Example, +); + +const template = OrgIntegrationBuilder.buildExampleTemplate( + OrganizationIntegrationServiceName.Example, +); + +await service.save(orgId, OrganizationIntegrationType.Example, config, template); +``` + +This creates a complete integration that can authenticate with the external service and format event data appropriately. diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index 050ec8df944..88af8081a8b 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -140,7 +140,10 @@ export class RiskInsightsOrchestratorService { reportProgress$ = this._reportProgressSubject.asObservable(); // --------------------------- Critical Application data --------------------- - criticalReportResults$: Observable = of(null); + private _criticalReportResultsSubject = new BehaviorSubject( + null, + ); + criticalReportResults$ = this._criticalReportResultsSubject.asObservable(); // --------------------------- Trigger subjects --------------------- private _initializeOrganizationTriggerSubject = new Subject(); @@ -989,7 +992,7 @@ export class RiskInsightsOrchestratorService { // Setup the pipeline to create a report view filtered to only critical applications private _setupCriticalApplicationReport() { const criticalReportResultsPipeline$ = this.enrichedReportData$.pipe( - filter((state) => !!state), + filter((state) => !!state && !!state.summaryData), map((enrichedReports) => { const criticalApplications = enrichedReports!.reportData.filter( (app) => app.isMarkedAsCritical, @@ -997,11 +1000,11 @@ export class RiskInsightsOrchestratorService { // Generate a new summary based on just the critical applications const summary = this.reportService.getApplicationsSummary( criticalApplications, - enrichedReports.applicationData, - enrichedReports.summaryData.totalMemberCount, + enrichedReports!.applicationData, + enrichedReports!.summaryData.totalMemberCount, ); return { - ...enrichedReports, + ...enrichedReports!, summaryData: summary, reportData: criticalApplications, }; @@ -1009,7 +1012,9 @@ export class RiskInsightsOrchestratorService { shareReplay({ bufferSize: 1, refCount: true }), ); - this.criticalReportResults$ = criticalReportResultsPipeline$; + criticalReportResultsPipeline$.pipe(takeUntil(this._destroy$)).subscribe((data) => { + this._criticalReportResultsSubject.next(data); + }); } /** diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts index 24d931165a7..e7c54bc81d0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts @@ -13,7 +13,7 @@ import { ButtonModule, ButtonType, LinkModule, TypographyModule } from "@bitward imports: [CommonModule, TypographyModule, JslibModule, LinkModule, ButtonModule], host: { class: - "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6 tw-h-56 tw-max-h-56", + "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6 tw-min-h-56 tw-overflow-hidden", }, }) export class ActivityCardComponent { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html index fe9880724f3..0bac4532bd9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html @@ -1,5 +1,5 @@
{{ "passwordChangeProgress" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html index 49b65c1916b..6bf36c74a71 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html @@ -2,9 +2,12 @@ } @else {