diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 9e59bc47853..a8ee1091a24 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -776,10 +776,18 @@ jobs: mkdir PlugIns cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/dmg/build/Release/safari.appex PlugIns/safari.appex + - name: Set up private auth key + run: | + mkdir ~/private_keys + cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 + ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + EOF + - name: Build application (dist) env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} + APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP + APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true run: npm run pack:mac @@ -985,12 +993,20 @@ jobs: mkdir PlugIns cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/mas/build/Release/safari.appex PlugIns/safari.appex + - name: Set up private auth key + run: | + mkdir ~/private_keys + cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 + ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + EOF + - name: Build application for App Store - run: npm run pack:mac:mas env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} + APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP + APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 CSC_FOR_PULL_REQUEST: true + run: npm run pack:mac:mas - name: Upload .pkg artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 @@ -1000,15 +1016,15 @@ jobs: if-no-files-found: error - name: Deploy to TestFlight - env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} if: | (github.ref == 'refs/heads/main' && needs.setup.outputs.rc_branch_exists == 0 && needs.setup.outputs.hotfix_branch_exists == 0) || (github.ref == 'refs/heads/rc' && needs.setup.outputs.hotfix_branch_exists == 0) || github.ref == 'refs/heads/hotfix-rc-desktop' + env: + APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} + APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP run: npm run upload:mas @@ -1180,11 +1196,18 @@ jobs: mkdir PlugIns cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/masdev/build/Release/safari.appex PlugIns/safari.appex + - name: Set up private auth key + run: | + mkdir ~/private_keys + cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 + ${{ secrets.APP_STORE_CONNECT_AUTH_KEY }} + EOF + - name: Build dev application for App Store - run: npm run pack:mac:masdev env: - APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }} + APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 + run: npm run pack:mac:masdev - name: Zip masdev asset run: | diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index ab08d509b37..f422c3560e6 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -1,25 +1,35 @@ ---- name: Chromatic on: push: - branches-ignore: - - 'renovate/**' - paths-ignore: - - '.github/workflows/**' + branches: + - "main" + - "rc" + - "hotfix-rc" + pull_request_target: + types: [opened, synchronize] jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + chromatic: name: Chromatic runs-on: ubuntu-22.04 + needs: check-run + permissions: + contents: read + pull-requests: write steps: - - name: Checkout repo + - name: Check out repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - - name: Get Node Version + - name: Get Node version id: retrieve-node-version run: | NODE_NVMRC=$(cat .nvmrc) @@ -31,7 +41,7 @@ jobs: with: node-version: ${{ steps.retrieve-node-version.outputs.node_version }} - - name: Cache npm + - name: Cache NPM id: npm-cache uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 with: @@ -41,7 +51,7 @@ jobs: - name: Install Node dependencies run: npm ci - # Manual build the storybook to resolve a chromatic/storybook bug related to TurboSnap + # Manually build the Storybook to resolve a bug related to TurboSnap - name: Build Storybook run: npm run build-storybook:ci diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 00000000000..3f9eb7b2e42 --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -0,0 +1,221 @@ +--- +name: Publish CLI +run-name: Publish CLI ${{ inputs.publish_type }} + +on: + workflow_dispatch: + inputs: + publish_type: + description: 'Publish Options' + required: true + default: 'Initial Publish' + type: choice + options: + - Initial Publish + - Republish + - Dry Run + version: + description: 'Version to publish (default: latest cli release)' + required: true + type: string + default: latest + snap_publish: + description: 'Publish to Snap store' + required: true + default: true + type: boolean + choco_publish: + description: 'Publish to Chocolatey store' + required: true + default: true + type: boolean + npm_publish: + description: 'Publish to npm registry' + required: true + default: true + type: boolean + + +defaults: + run: + working-directory: apps/cli + +jobs: + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + release-version: ${{ steps.version-output.outputs.version }} + deployment-id: ${{ steps.deployment.outputs.deployment-id }} + steps: + - name: Version output + id: version-output + run: | + if [[ "${{ github.event.inputs.version }}" == "latest" || "${{ github.event.inputs.version }}" == "" ]]; then + VERSION=$(curl "https://api.github.com/repos/bitwarden/clients/releases" | jq -c '.[] | select(.tag_name | contains("cli")) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+') + echo "Latest Released Version: $VERSION" + echo "::set-output name=version::$VERSION" + else + echo "Release Version: ${{ github.event.inputs.version }}" + echo "::set-output name=version::${{ github.event.inputs.version }}" + fi + + - name: Create GitHub deployment + if: ${{ github.event.inputs.release_type != 'Dry Run' }} + uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 + id: deployment + with: + token: '${{ secrets.GITHUB_TOKEN }}' + initial-status: 'in_progress' + environment: 'CLI - Production' + description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ github.ref_name }}' + task: release + + snap: + name: Deploy Snap + runs-on: ubuntu-22.04 + needs: setup + if: inputs.snap_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "snapcraft-store-token" + + - name: Install Snap + uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 + + - name: Download artifacts + run: wget https://github.com/bitwarden/clients/releases/cli-v${{ env._PKG_VERSION }}/download/bw_${{ env._PKG_VERSION }}_amd64.snap + + - name: Publish Snap & logout + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} + run: | + snapcraft upload bw_${{ env._PKG_VERSION }}_amd64.snap --release stable + snapcraft logout + + choco: + name: Deploy Choco + runs-on: windows-2022 + needs: setup + if: inputs.choco_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "cli-choco-api-key" + + - name: Setup Chocolatey + run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ + env: + CHOCO_API_KEY: ${{ steps.retrieve-secrets.outputs.cli-choco-api-key }} + + - name: Make dist dir + shell: pwsh + run: New-Item -ItemType directory -Path ./dist + + - name: Download artifacts + run: wget https://github.com/bitwarden/clients/releases/cli-v${{ env._PKG_VERSION }}/download/bitwarden-cli.${{ env._PKG_VERSION }}.nupkg + + - name: Push to Chocolatey + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + shell: pwsh + run: | + cd dist + choco push --source=https://push.chocolatey.org/ + + npm: + name: Publish NPM + runs-on: ubuntu-22.04 + needs: setup + if: inputs.npm_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "npm-api-key" + + - name: Download artifacts + run: wget https://github.com/bitwarden/clients/releases/cli-v${{ env._PKG_VERSION }}/download/bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip + + - name: Setup NPM + run: | + echo 'registry="https://registry.npmjs.org/"' > ./.npmrc + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc + env: + NPM_TOKEN: ${{ steps.retrieve-secrets.outputs.npm-api-key }} + + - name: Install Husky + run: npm install -g husky + + - name: Publish NPM + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + run: npm publish --access public --regsitry=https://registry.npmjs.org/ --userconfig=./.npmrc + + update-deployment: + name: Update Deployment Status + runs-on: ubuntu-22.04 + needs: + - setup + - npm + - snap + - choco + if: ${{ always() && github.event.inputs.publish_type != 'Dry Run' }} + steps: + - name: Check if any job failed + if: contains(needs.*.result, 'failure') + run: exit 1 + + - name: Update deployment status to Success + if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'success' + deployment-id: ${{ needs.setup.outputs.deployment-id }} + + - name: Update deployment status to Failure + if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'failure' + deployment-id: ${{ needs.setup.outputs.deployment-id }} \ No newline at end of file diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml new file mode 100644 index 00000000000..2c4e467bc2a --- /dev/null +++ b/.github/workflows/publish-desktop.yml @@ -0,0 +1,296 @@ +--- +name: Publish Desktop +run-name: Publish Desktop ${{ inputs.publish_type }} + +on: + workflow_dispatch: + inputs: + publish_type: + description: 'Publish Options' + required: true + default: 'Initial Publish' + type: choice + options: + - Initial Publish + - Republish + - Dry Run + version: + description: 'Version to publish (default: latest cli release)' + required: true + type: string + default: latest + rollout_percentage: + description: 'Staged Rollout Percentage' + required: true + default: '10' + type: string + snap_publish: + description: 'Publish to Snap store' + required: true + default: true + type: boolean + choco_publish: + description: 'Publish to Chocolatey store' + required: true + default: true + type: boolean + +defaults: + run: + shell: bash + +jobs: + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + release-version: ${{ steps.version.outputs.version }} + release-channel: ${{ steps.release-channel.outputs.channel }} + tag-name: ${{ steps.version.outputs.tag_name }} + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + steps: + - name: Check Publish Version + id: version + run: | + if [[ "${{ github.event.inputs.version }}" == "latest" || "${{ github.event.inputs.version }}" == "" ]]; then + TAG_NAME=$(curl "https://api.github.com/repos/bitwarden/clients/releases" | jq -c '.[] | select(.tag_name | contains("desktop")) | .tag_name' | head -1 | cut -d '"' -f 2) + VERSION=$(echo $TAG_NAME | sed "s/desktop-v//") + echo "Latest Released Version: $VERSION" + echo "::set-output name=version::$VERSION" + + echo "Tag name: $TAG_NAME" + echo "::set-output name=tag_name::$TAG_NAME" + else + echo "Release Version: ${{ github.event.inputs.version }}" + echo "::set-output name=version::${{ github.event.inputs.version }}" + + $TAG_NAME="desktop-v${{ github.event.inputs.version }}" + + echo "Tag name: $TAG_NAME" + echo "::set-output name=tag_name::$TAG_NAME" + fi + + - name: Get Version Channel + id: release-channel + run: | + case "${{ steps.version.outputs.version }}" in + *"alpha"*) + echo "channel=alpha" >> $GITHUB_OUTPUT + echo "[!] We do not yet support 'alpha'" + exit 1 + ;; + *"beta"*) + echo "channel=beta" >> $GITHUB_OUTPUT + ;; + *) + echo "channel=latest" >> $GITHUB_OUTPUT + ;; + esac + + - name: Create GitHub deployment + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 + id: deployment + with: + token: '${{ secrets.GITHUB_TOKEN }}' + initial-status: 'in_progress' + environment: 'Desktop - Production' + description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}' + task: release + + electron-blob: + name: Electron blob publish + runs-on: ubuntu-22.04 + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + steps: + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "aws-electron-access-id, + aws-electron-access-key, + aws-electron-bucket-name" + + - name: Download all artifacts + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + workflow: build-desktop.yml + workflow_conclusion: success + branch: ${{ github.ref_name }} + path: apps/desktop/artifacts + + - name: Download artifacts + working-directory: apps/desktop/artifacts + run: gh release download ${{ env._RELEASE_TAG }} -R bitwarden/desktop + + - name: Set staged rollout percentage + env: + RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} + ROLLOUT_PCT: ${{ inputs.rollout_percentage }} + run: | + echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml + echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml + echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml + + - name: Publish artifacts to S3 + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} + AWS_DEFAULT_REGION: 'us-west-2' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} + working-directory: apps/desktop/artifacts + run: | + aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + --acl "public-read" \ + --recursive \ + --quiet + + - name: Update deployment status to Success + if: ${{ github.event.inputs.publish_type != 'Dry Run' && success() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'success' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + - name: Update deployment status to Failure + if: ${{ github.event.inputs.publish_type != 'Dry Run' && failure() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'failure' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + snap: + name: Deploy Snap + runs-on: ubuntu-22.04 + needs: setup + if: ${{ github.event.inputs.snap_publish == 'true' }} + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + steps: + - name: Checkout Repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "snapcraft-store-token" + + - name: Install Snap + uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 + + - name: Setup + run: mkdir dist + working-directory: apps/desktop + + - name: Download artifacts + working-directory: apps/desktop/dist + run: wget https://github.com/bitwarden/clients/releases/${{ env._RELEASE_TAG }}/download/bitwarden_${{ env._PKG_VERSION }}_amd64.snap + + - name: Deploy to Snap Store + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} + run: | + snapcraft upload bitwarden_${{ env._PKG_VERSION }}_amd64.snap --release stable + snapcraft logout + working-directory: apps/desktop/dist + + choco: + name: Deploy Choco + runs-on: windows-2022 + needs: setup + if: ${{ github.event.inputs.choco_publish == 'true' }} + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + steps: + - name: Checkout Repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Print Environment + run: | + dotnet --version + dotnet nuget --version + + - name: Login to Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets + id: retrieve-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "cli-choco-api-key" + + - name: Setup Chocolatey + shell: pwsh + run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ + env: + CHOCO_API_KEY: ${{ steps.retrieve-secrets.outputs.cli-choco-api-key }} + + - name: Make dist dir + shell: pwsh + run: New-Item -ItemType directory -Path ./dist + working-directory: apps/desktop + + - name: Download artifacts + working-directory: apps/desktop/dist + run: wget https://github.com/bitwarden/clients/releases/${{ env._RELEASE_TAG }}/download/bitwarden.${{ env._PKG_VERSION }}.nupkg + + - name: Push to Chocolatey + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + shell: pwsh + run: choco push --source=https://push.chocolatey.org/ + working-directory: apps/desktop/dist + + update-deployment: + name: Update Deployment Status + runs-on: ubuntu-22.04 + needs: + - setup + - electron-blob + - snap + - choco + if: ${{ always() && github.event.inputs.publish_type != 'Dry Run' }} + steps: + - name: Check if any job failed + if: contains(needs.*.result, 'failure') + run: exit 1 + + - name: Update deployment status to Success + if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'success' + deployment-id: ${{ needs.setup.outputs.deployment-id }} + + - name: Update deployment status to Failure + if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + state: 'failure' + deployment-id: ${{ needs.setup.outputs.deployment-id }} diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml new file mode 100644 index 00000000000..733e3945e5f --- /dev/null +++ b/.github/workflows/publish-web.yml @@ -0,0 +1,144 @@ +--- +name: Publish Web +run-name: Publish Web ${{ inputs.publish_type }} + +on: + workflow_dispatch: + inputs: + publish_type: + description: 'Publish Options' + required: true + default: 'Initial Publish' + type: choice + options: + - Initial Publish + - Redeploy + - Dry Run + +env: + _AZ_REGISTRY: bitwardenprod.azurecr.io + +jobs: + setup: + name: Setup + runs-on: ubuntu-22.04 + outputs: + release_version: ${{ steps.version.outputs.version }} + tag_version: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Branch check + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-web" ]]; then + echo "===================================" + echo "[!] Can only release from the 'rc' or 'hotfix-rc-web' branches" + echo "===================================" + exit 1 + fi + + - name: Check Release Version + id: version + uses: bitwarden/gh-actions/release-version-check@main + with: + release-type: ${{ github.event.inputs.publish_type }} + project-type: ts + file: apps/web/package.json + monorepo: true + monorepo-project: web + + self-host: + name: Release self-host docker + runs-on: ubuntu-22.04 + needs: setup + env: + _BRANCH_NAME: ${{ github.ref_name }} + _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_OPTION: ${{ github.event.inputs.publish_type }} + steps: + - name: Print environment + run: | + whoami + docker --version + echo "GitHub ref: $GITHUB_REF" + echo "GitHub event: $GITHUB_EVENT" + echo "Github Release Option: $_RELEASE_OPTION" + + - name: Checkout repo + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + ########## ACR ########## + - name: Login to Azure - PROD Subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + + - name: Login to Azure ACR + run: az acr login -n bitwardenprod + + - name: Create GitHub deployment + if: ${{ github.event.inputs.publish_type != 'Dry Run' }} + uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 + id: deployment + with: + token: '${{ secrets.GITHUB_TOKEN }}' + initial-status: 'in_progress' + environment-url: http://vault.bitwarden.com + environment: 'Web Vault - US Production Cloud' + description: 'Deployment ${{ needs.setup.outputs.release_version }} from branch ${{ github.ref_name }}' + task: release + + - name: Pull branch image + run: | + if [[ "${{ github.event.inputs.publish_type }}" == "Dry Run" ]]; then + docker pull $_AZ_REGISTRY/web:latest + else + docker pull $_AZ_REGISTRY/web:$_BRANCH_NAME + fi + + - name: Tag version + run: | + if [[ "${{ github.event.inputs.publish_type }}" == "Dry Run" ]]; then + docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web:dryrun + docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web-sh:dryrun + else + docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:$_RELEASE_VERSION + docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION + docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:latest + docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:latest + fi + + - name: Push version + run: | + if [[ "${{ github.event.inputs.publish_type }}" == "Dry Run" ]]; then + docker push $_AZ_REGISTRY/web:dryrun + docker push $_AZ_REGISTRY/web-sh:dryrun + else + docker push $_AZ_REGISTRY/web:$_RELEASE_VERSION + docker push $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION + docker push $_AZ_REGISTRY/web:latest + docker push $_AZ_REGISTRY/web-sh:latest + fi + + - name: Update deployment status to Success + if: ${{ github.event.inputs.publish_type != 'Dry Run' && success() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + environment-url: http://vault.bitwarden.com + state: 'success' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + - name: Update deployment status to Failure + if: ${{ github.event.inputs.publish_type != 'Dry Run' && failure() }} + uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + environment-url: http://vault.bitwarden.com + state: 'failure' + deployment-id: ${{ steps.deployment.outputs.deployment_id }} + + - name: Log out of Docker + run: docker logout diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 68c33ca358e..3feaff8cede 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -91,16 +91,6 @@ jobs: - setup - locales-test steps: - - name: Create GitHub deployment - uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 - id: deployment - with: - token: '${{ secrets.GITHUB_TOKEN }}' - initial-status: 'in_progress' - environment: 'Browser - Production' - description: 'Deployment ${{ needs.setup.outputs.release-version }} from branch ${{ github.ref_name }}' - task: release - - name: Download latest Release build artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -152,19 +142,3 @@ jobs: body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true - - - name: Update deployment status to Success - if: ${{ success() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'success' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - - name: Update deployment status to Failure - if: ${{ failure() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'failure' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 6d56c3be831..fe402e7a8f1 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -14,22 +14,6 @@ on: - Initial Release - Redeploy - Dry Run - snap_publish: - description: 'Publish to Snap store' - required: true - default: true - type: boolean - choco_publish: - description: 'Publish to Chocolatey store' - required: true - default: true - type: boolean - npm_publish: - description: 'Publish to npm registry' - required: true - default: true - type: boolean - defaults: run: @@ -65,17 +49,11 @@ jobs: monorepo: true monorepo-project: cli - - name: Create GitHub deployment - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 - id: deployment - with: - token: '${{ secrets.GITHUB_TOKEN }}' - initial-status: 'in_progress' - environment: 'CLI - Production' - description: 'Deployment ${{ steps.version.outputs.version }} from branch ${{ github.ref_name }}' - task: release - + release: + name: Release + runs-on: ubuntu-22.04 + needs: setup + steps: - name: Download all Release artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -121,189 +99,3 @@ jobs: body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true - - - name: Update deployment status to Success - if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'success' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - - name: Update deployment status to Failure - if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'failure' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - snap: - name: Deploy Snap - runs-on: ubuntu-22.04 - needs: setup - if: inputs.snap_publish - env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - steps: - - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "snapcraft-store-token" - - - name: Install Snap - uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 - - - name: Download artifacts - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli - workflow_conclusion: success - branch: ${{ github.ref_name }} - artifacts: bw_${{ env._PKG_VERSION }}_amd64.snap - - - name: Dry Run - Download artifacts - if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli - workflow_conclusion: success - branch: main - artifacts: bw_${{ env._PKG_VERSION }}_amd64.snap - - - name: Publish Snap & logout - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} - run: | - snapcraft upload bw_${{ env._PKG_VERSION }}_amd64.snap --release stable - snapcraft logout - - choco: - name: Deploy Choco - runs-on: windows-2022 - needs: setup - if: inputs.choco_publish - env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - steps: - - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "cli-choco-api-key" - - - name: Setup Chocolatey - run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ - env: - CHOCO_API_KEY: ${{ steps.retrieve-secrets.outputs.cli-choco-api-key }} - - - name: Make dist dir - shell: pwsh - run: New-Item -ItemType directory -Path ./dist - - - name: Download artifacts - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli/dist - workflow_conclusion: success - branch: ${{ github.ref_name }} - artifacts: bitwarden-cli.${{ env._PKG_VERSION }}.nupkg - - - name: Dry Run - Download artifacts - if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli/dist - workflow_conclusion: success - branch: main - artifacts: bitwarden-cli.${{ env._PKG_VERSION }}.nupkg - - - name: Push to Chocolatey - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - shell: pwsh - run: | - cd dist - choco push --source=https://push.chocolatey.org/ - - npm: - name: Publish NPM - runs-on: ubuntu-22.04 - needs: setup - if: inputs.npm_publish - env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - steps: - - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "npm-api-key" - - - name: Download artifacts - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli/build - workflow_conclusion: success - branch: ${{ github.ref_name }} - artifacts: bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip - - - name: Dry Run - Download artifacts - if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-cli.yml - path: apps/cli/build - workflow_conclusion: success - branch: main - artifacts: bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip - - - name: Setup NPM - run: | - echo 'registry="https://registry.npmjs.org/"' > ./.npmrc - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc - env: - NPM_TOKEN: ${{ steps.retrieve-secrets.outputs.npm-api-key }} - - - name: Install Husky - run: npm install -g husky - - - name: Publish NPM - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - run: npm publish --access public --regsitry=https://registry.npmjs.org/ --userconfig=./.npmrc diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index eb63a53f2ea..c9e1df94026 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -6,34 +6,13 @@ on: workflow_dispatch: inputs: release_type: - description: 'Release Options' + description: 'Release Type' required: true - default: 'Initial Release' + default: 'Release' type: choice options: - - Initial Release - - Redeploy + - Release - Dry Run - rollout_percentage: - description: 'Staged Rollout Percentage' - required: true - default: '10' - type: string - snap_publish: - description: 'Publish to Snap store' - required: true - default: true - type: boolean - choco_publish: - description: 'Publish to Chocolatey store' - required: true - default: true - type: boolean - github_release: - description: 'Publish GitHub release' - required: true - default: true - type: boolean defaults: run: @@ -87,31 +66,6 @@ jobs: ;; esac - - name: Create GitHub deployment - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 - id: deployment - with: - token: '${{ secrets.GITHUB_TOKEN }}' - initial-status: 'in_progress' - environment: 'Desktop - Production' - description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}' - task: release - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "aws-electron-access-id, - aws-electron-access-key, - aws-electron-bucket-name" - - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -136,29 +90,6 @@ jobs: working-directory: apps/desktop/artifacts run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive - - name: Set staged rollout percentage - env: - RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} - ROLLOUT_PCT: ${{ inputs.rollout_percentage }} - run: | - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml - - - name: Publish artifacts to S3 - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} - AWS_DEFAULT_REGION: 'us-west-2' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --acl "public-read" \ - --recursive \ - --quiet - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: @@ -203,143 +134,3 @@ jobs: body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true - - - name: Update deployment status to Success - if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'success' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - - name: Update deployment status to Failure - if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - state: 'failure' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - snap: - name: Deploy Snap - runs-on: ubuntu-22.04 - needs: setup - if: ${{ github.event.inputs.snap_publish == 'true' }} - env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - steps: - - name: Checkout Repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "snapcraft-store-token" - - - name: Install Snap - uses: samuelmeuli/action-snapcraft@d33c176a9b784876d966f80fb1b461808edc0641 # v2.1.1 - - - name: Setup - run: mkdir dist - working-directory: apps/desktop - - - name: Download Snap artifact - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-desktop.yml - workflow_conclusion: success - branch: ${{ github.ref_name }} - artifacts: bitwarden_${{ env._PKG_VERSION }}_amd64.snap - path: apps/desktop/dist - - - name: Dry Run - Download Snap artifact - if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-desktop.yml - workflow_conclusion: success - branch: main - artifacts: bitwarden_${{ env._PKG_VERSION }}_amd64.snap - path: apps/desktop/dist - - - name: Deploy to Snap Store - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} - run: | - snapcraft upload bitwarden_${{ env._PKG_VERSION }}_amd64.snap --release stable - snapcraft logout - working-directory: apps/desktop/dist - - choco: - name: Deploy Choco - runs-on: windows-2022 - needs: setup - if: ${{ github.event.inputs.choco_publish == 'true' }} - env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - steps: - - name: Checkout Repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Print Environment - run: | - dotnet --version - dotnet nuget --version - - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "cli-choco-api-key" - - - name: Setup Chocolatey - shell: pwsh - run: choco apikey --key $env:CHOCO_API_KEY --source https://push.chocolatey.org/ - env: - CHOCO_API_KEY: ${{ steps.retrieve-secrets.outputs.cli-choco-api-key }} - - - name: Make dist dir - shell: pwsh - run: New-Item -ItemType directory -Path ./dist - working-directory: apps/desktop - - - name: Download choco artifact - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-desktop.yml - workflow_conclusion: success - branch: ${{ github.ref_name }} - artifacts: bitwarden.${{ env._PKG_VERSION }}.nupkg - path: apps/desktop/dist - - - name: Dry Run - Download choco artifact - if: ${{ github.event.inputs.release_type == 'Dry Run' }} - uses: bitwarden/gh-actions/download-artifacts@main - with: - workflow: build-desktop.yml - workflow_conclusion: success - branch: main - artifacts: bitwarden.${{ env._PKG_VERSION }}.nupkg - path: apps/desktop/dist - - - name: Push to Chocolatey - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - shell: pwsh - run: choco push --source=https://push.chocolatey.org/ - working-directory: apps/desktop/dist diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 2da6daaa19c..596341459cd 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -15,9 +15,6 @@ on: - Redeploy - Dry Run -env: - _AZ_REGISTRY: bitwardenprod.azurecr.io - jobs: setup: name: Setup @@ -49,89 +46,12 @@ jobs: monorepo: true monorepo-project: web - self-host: - name: Release self-host docker - runs-on: ubuntu-22.04 - needs: setup - env: - _BRANCH_NAME: ${{ github.ref_name }} - _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} - _RELEASE_OPTION: ${{ github.event.inputs.release_type }} - steps: - - name: Print environment - run: | - whoami - docker --version - echo "GitHub ref: $GITHUB_REF" - echo "GitHub event: $GITHUB_EVENT" - echo "Github Release Option: $_RELEASE_OPTION" - - - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - ########## ACR ########## - - name: Login to Azure - PROD Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - - - name: Login to Azure ACR - run: az acr login -n bitwardenprod - - - name: Pull branch image - run: | - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker pull $_AZ_REGISTRY/web:latest - else - docker pull $_AZ_REGISTRY/web:$_BRANCH_NAME - fi - - - name: Tag version - run: | - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web:dryrun - docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web-sh:dryrun - else - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:$_RELEASE_VERSION - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:latest - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:latest - fi - - - name: Push version - run: | - if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then - docker push $_AZ_REGISTRY/web:dryrun - docker push $_AZ_REGISTRY/web-sh:dryrun - else - docker push $_AZ_REGISTRY/web:$_RELEASE_VERSION - docker push $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION - docker push $_AZ_REGISTRY/web:latest - docker push $_AZ_REGISTRY/web-sh:latest - fi - - - name: Log out of Docker - run: docker logout - release: name: Create GitHub Release runs-on: ubuntu-22.04 needs: - setup - - self-host steps: - - name: Create GitHub deployment - if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 - id: deployment - with: - token: '${{ secrets.GITHUB_TOKEN }}' - initial-status: 'in_progress' - environment-url: http://vault.bitwarden.com - environment: 'Web Vault - US Production Cloud' - description: 'Deployment ${{ needs.setup.outputs.release_version }} from branch ${{ github.ref_name }}' - task: release - - name: Download latest build artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -172,21 +92,3 @@ jobs: apps/web/artifacts/web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip" token: ${{ secrets.GITHUB_TOKEN }} draft: true - - - name: Update deployment status to Success - if: ${{ github.event.inputs.release_type != 'Dry Run' && success() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: http://vault.bitwarden.com - state: 'success' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} - - - name: Update deployment status to Failure - if: ${{ github.event.inputs.release_type != 'Dry Run' && failure() }} - uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: http://vault.bitwarden.com - state: 'failure' - deployment-id: ${{ steps.deployment.outputs.deployment_id }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16238f15308..909bb93f683 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,9 +11,28 @@ on: types: [opened, synchronize] jobs: - test: + check-test-secrets: + name: Check for test secrets + runs-on: ubuntu-22.04 + outputs: + available: ${{ steps.check-test-secrets.outputs.available }} + permissions: + contents: read + + steps: + - name: Check + id: check-test-secrets + run: | + if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then + echo "available=true" >> $GITHUB_OUTPUT; + else + echo "available=false" >> $GITHUB_OUTPUT; + fi + + testing: name: Run tests runs-on: ubuntu-22.04 + needs: check-test-secrets permissions: checks: write contents: read @@ -57,26 +76,17 @@ jobs: run: npm test -- --coverage --maxWorkers=3 - name: Report test results - uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0 - if: always() + uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 + if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }} with: name: Test Results path: "junit.xml" reporter: jest-junit fail-on-error: true - - name: Check for Codecov secret - id: check-codecov-secret - run: | - if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then - echo "available=true" >> $GITHUB_OUTPUT; - else - echo "available=false" >> $GITHUB_OUTPUT; - fi - - name: Upload to codecov.io uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 - if: steps.check-codecov-secret.outputs.available == 'true' + if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/apps/browser/package.json b/apps/browser/package.json index bbcf0badbc5..a210b6353c4 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.7.1", + "version": "2024.8.0", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 48e7de085fe..0373055d1bf 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "تحرير المجلّد" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "حذف المجلّد" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "واحدة أو أكثر من سياسات المؤسسة تؤثر على إعدادات المولدات الخاصة بك." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "إجراء مهلة المخزن" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "عدم تطابق الحساب" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "لم يتم إعداد القياسات الحيوية" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 09e43ab2dc5..7f62c82ae24 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Qovluğa düzəliş et" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Qovluğu sil" }, @@ -1343,13 +1361,13 @@ "message": "Anbarı yan çubuqda aç" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Hazırkı veb sayt üçün son istifadə edilən girişi avto-doldur" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Hazırkı veb sayt üçün son istifadə edilən kartı avto-doldur" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Hazırkı veb sayt üçün son istifadə edilən kimliyi avto-doldur" }, "commandGeneratePasswordDesc": { "message": "Təsadüfi yeni bir parol yarat və lövhəyə kopyala" @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Baza domeni (tövsiyə edilən)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Bir və ya daha çox təşkilat siyasəti yaradıcı ayarlarınıza təsir edir." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Anbar vaxtının bitmə əməliyyatı" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Hesablar uyuşmur" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometriklər qurulmayıb" }, @@ -2328,7 +2375,7 @@ "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Anbara müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz 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." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Anbarınıza müraciət etmək üçün lütfən ana parol təyin edin." }, "resetPasswordPolicyAutoEnroll": { "message": "Avtomatik yazılma" @@ -2787,16 +2834,16 @@ "message": "Qısayolu dəyişdir" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Qısayolları idarə et" }, "autofillShortcut": { "message": "Avto-doldurma klaviatura qısayolu" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "Avto-doldurma giriş qısayolu ayarlanmayıb. Bunu brauzerin ayarlarında dəyişdirin." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "Girişin avto-doldurma qısayolu: $COMMAND$. Bütün qısayolları brauzerin ayarlarında idarə edin.", "placeholders": { "command": { "content": "$1", @@ -3831,22 +3878,22 @@ "message": "Kimlik doğrulayıcı açarı" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Avto-doldurma seçimləri" }, "websiteUri": { - "message": "Website (URI)" + "message": "Veb sayt (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Veb sayt əlavə edildi" }, "addWebsite": { - "message": "Add website" + "message": "Veb sayt əlavə et" }, "deleteWebsite": { - "message": "Delete website" + "message": "Veb saytı sil" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "İlkin ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "$WEBSITE$ ilə uyuşma aşkarlamasını göstər", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "$WEBSITE$ ilə uyuşma aşkarlamasını gizlət", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Səhifə yüklənəndə avto-doldurulsun?" }, "cardDetails": { "message": "Kart detalları" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Təyin et" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Element yeri" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden-in yeni bir görünüşü var!" }, @@ -4098,6 +4163,12 @@ "message": "Anbar vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" }, "accountActions": { - "message": "Account actions" + "message": "Hesab fəaliyyətləri" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 2dc68605199..4e78a92e8e1 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Рэдагаваць папку" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Выдаліць папку" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Дзеянне пасля заканчэння часу чакання сховішча" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Неадпаведнасць уліковых запісаў" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Біяметрыя не ўключана" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index b1fc24ac0f3..454988dda71 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Редактиране на папка" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Изтриване на папка" }, @@ -823,7 +841,7 @@ "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "message": "Показване на самоличности като предложения за самопопълване в изгледа на трезора" }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" @@ -1489,7 +1507,7 @@ "message": "Д-р" }, "mx": { - "message": "Mx" + "message": "Госпоуи" }, "firstName": { "message": "Собствено име" @@ -1567,7 +1585,7 @@ "message": "Самоличност" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ново $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Поне една политика на организация влияе на настройките на генерирането на паролите." }, + "passwordGenerator": { + "message": "Генератор на пароли" + }, + "usernameGenerator": { + "message": "Генератор на потребителски имена" + }, + "useThisPassword": { + "message": "Използване на тази парола" + }, + "useThisUsername": { + "message": "Използване на това потребителско име" + }, + "securePasswordGenerated": { + "message": "Създадена е сигурна парола! Не забравяйте и да промените паролата си в уеб сайта." + }, + "useGeneratorHelpTextPartOne": { + "message": "Използвайте генератора", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "за да създадете сигурна и уникална парола", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Действие при изтичане на времето" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Регистрациите са различни" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Отключването чрез биометрични данни не беше успешно. Биометричният таен ключ не успя да отключи трезора. Опитайте да направите настройката на биометричните данни отново." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Несъответстващ биометричен ключ" + }, "biometricsNotEnabledTitle": { "message": "Потвърждаването с биометрични данни не е включено" }, @@ -2328,7 +2375,7 @@ "message": "Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Вашата организация е деактивирала шифроването чрез доверени устройства. Задайте главна парола, за да получите достъп до трезора си." }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматично включване" @@ -3264,7 +3311,7 @@ "message": "Неправилна парола за файла. Използвайте паролата, която сте въвели при създаването на изнесения файл." }, "destination": { - "message": "Destination" + "message": "Местоназначение" }, "learnAboutImportOptions": { "message": "Научете повече относно възможностите за внасяне" @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Показване на откритото съвпадение $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Скриване на откритото съвпадение $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3897,6 +3944,18 @@ "data": { "message": "Данни" }, + "passkeys": { + "message": "Секретни ключове", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Пароли", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Вписване със секретен ключ", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Свързване" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Местоположение на елемента" }, + "fileSends": { + "message": "Файлови изпращания" + }, + "textSends": { + "message": "Текстови изпращания" + }, "bitwardenNewLook": { "message": "Биуорден има нов облик!" }, @@ -4098,6 +4163,12 @@ "message": "Сега е по-лесно и интуитивно от всякога да използвате автоматичното попълване и да търсите в раздела на трезора. Разгледайте!" }, "accountActions": { - "message": "Account actions" + "message": "Действия по регистрацията" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index efba4adaaa1..d6bc891a172 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "ফোল্ডার সম্পাদনা" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "ফোল্ডার মুছুন" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "এক বা একাধিক সংস্থার নীতিগুলি আপনার উৎপাদকের সেটিংসকে প্রভাবিত করছে।" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "ভল্টের সময়সীমা কর্ম" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "অ্যাকাউন্ট মেলেনি" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "বায়োমেট্রিকস সক্ষম নেই" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 60a0fced110..198874339e9 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 66a089e08ee..fe0ec495b8a 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edita la carpeta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Suprimeix carpeta" }, @@ -636,7 +654,7 @@ "message": "El codi de verificació és obligatori." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "L'autenticació s'ha cancel·lat o ha tardat massa. Torna-ho a provar." }, "invalidVerificationCode": { "message": "Codi de verificació no vàlid" @@ -664,10 +682,10 @@ "message": "Escaneja el codi QR de l'autenticador des de la pàgina web actual" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Feu que la verificació en dos passos siga perfecta" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden pot emmagatzemar i omplir codis de verificació en dos passos. Copieu i enganxeu la clau en aquest camp." }, "totpHelperWithCapture": { "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." @@ -685,7 +703,7 @@ "message": "La vostra sessió ha caducat." }, "logIn": { - "message": "Log in" + "message": "Inicia sessió" }, "restartRegistration": { "message": "Restart registration" @@ -761,7 +779,7 @@ "message": "Nova URI" }, "addDomain": { - "message": "Add domain", + "message": "Afig domini", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -873,7 +891,7 @@ "message": "Desbloqueja" }, "additionalOptions": { - "message": "Additional options" + "message": "Opcions addicionals" }, "enableContextMenuItem": { "message": "Mostra les opcions del menú contextual" @@ -913,7 +931,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Exporta des de" }, "exportVault": { "message": "Exporta caixa forta" @@ -925,25 +943,25 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Contrasenya del fitxer" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Aquesta contrasenya s'utilitzarà per exportar i importar aquest fitxer" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Utilitzeu la clau de xifratge del vostre compte, derivada del nom d'usuari i la contrasenya mestra, per xifrar l'exportació i restringir la importació només al compte de Bitwarden actual." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Establiu una contrasenya per xifrar l'exportació i importeu-la a qualsevol compte de Bitwarden mitjançant aquesta contrasenya." }, "exportTypeHeading": { - "message": "Export type" + "message": "Tipus d'exportació" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hi ha una restricció de compte" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Contrasenya del fitxer\" i \"Confirma contrasenya del fitxer\" no coincideixen." }, "warning": { "message": "ADVERTIMENT", @@ -1382,7 +1400,7 @@ "message": "Booleà" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Casella de selecció" }, "cfTypeLinked": { "message": "Enllaçat", @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una o més polítiques d’organització afecten la configuració del generador." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Acció quan acabe el temps d'espera de la caixa forta" }, @@ -1938,7 +1979,7 @@ "message": "By continuing, you agree to the" }, "and": { - "message": "and" + "message": "i" }, "acceptPolicies": { "message": "Si activeu aquesta casella, indiqueu que esteu d’acord amb el següent:" @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "El compte no coincideix" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "La biomètrica no està habilitada" }, @@ -3702,7 +3749,7 @@ } }, "new": { - "message": "New" + "message": "Nou" }, "removeItem": { "message": "Remove $NAME$", @@ -3718,10 +3765,10 @@ "message": "Items with no folder" }, "itemDetails": { - "message": "Item details" + "message": "Detalls de l'element" }, "itemName": { - "message": "Item name" + "message": "Nom d'element" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -3736,29 +3783,29 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Propietari" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tú", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "No es pot accedir als elements de les organitzacions inhabilitades. Poseu-vos en contacte amb el propietari de la vostra organització per obtenir ajuda." }, "additionalInformation": { - "message": "Additional information" + "message": "Informació addicional" }, "itemHistory": { "message": "Item history" }, "lastEdited": { - "message": "Last edited" + "message": "Última edició" }, "ownerYou": { "message": "Owner: You" }, "linked": { - "message": "Linked" + "message": "Enllaçat" }, "copySuccessful": { "message": "Copy Successful" @@ -3800,16 +3847,16 @@ "message": "Free organizations cannot use attachments" }, "filters": { - "message": "Filters" + "message": "Filtres" }, "personalDetails": { - "message": "Personal details" + "message": "Detalls personals" }, "identification": { - "message": "Identification" + "message": "Identificació" }, "contactInfo": { - "message": "Contact info" + "message": "Informació de contacte" }, "downloadAttachment": { "message": "Download - $ITEMNAME$", @@ -3825,28 +3872,28 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Credencials d'inici de sessió" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Clau autenticadora" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Opcions d'emplenament automàtic" }, "websiteUri": { - "message": "Website (URI)" + "message": "Lloc web (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Lloc web afegit" }, "addWebsite": { - "message": "Add website" + "message": "Afig un lloc web" }, "deleteWebsite": { - "message": "Delete website" + "message": "Suprimeix lloc web" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Per defecte ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3889,16 +3936,28 @@ } }, "addAccount": { - "message": "Add account" + "message": "Afig compte" }, "loading": { - "message": "Loading" + "message": "S'està carregant" }, "data": { - "message": "Data" + "message": "Dades" + }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "Assigna" }, "bulkCollectionAssignmentDialogDescriptionSingular": { "message": "Only organization members with access to these collections will be able to see the item." @@ -3919,16 +3978,16 @@ } }, "addField": { - "message": "Add field" + "message": "Afig un camp" }, "add": { - "message": "Add" + "message": "Afig" }, "fieldType": { - "message": "Field type" + "message": "Tipus de camp" }, "fieldLabel": { - "message": "Field label" + "message": "Etiqueta del camp" }, "textHelpText": { "message": "Use text fields for data like security questions" @@ -3946,7 +4005,7 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "editField": { - "message": "Edit field" + "message": "Edita el camp" }, "editFieldLabel": { "message": "Edit $LABEL$", @@ -4002,7 +4061,7 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Seleccioneu les col·leccions per assignar" }, "personalItemTransferWarningSingular": { "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 93f2f991a8d..c7afbeeac61 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Upravit složku" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Smazat složku" }, @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Základní doména (doporučeno)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Jedna nebo více zásad organizace ovlivňují nastavení generátoru." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Akce při vypršení časového limitu" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Neshoda účtů" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrie není nastavena" }, @@ -2328,7 +2375,7 @@ "message": "Vaše hlavní heslo nesplňuje jednu nebo více zásad Vaší organizace. Pro přístup k trezoru musíte nyní aktualizovat své hlavní heslo. Pokračování Vás odhlásí z Vaší aktuální relace a bude nutné se přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Vaše organizace zakázala šifrování pomocí důvěryhodného zařízení. Nastavte hlavní heslo pro přístup k trezoru." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrace" @@ -3831,22 +3878,22 @@ "message": "Ověřovací klíč" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Nastavení automatického vyplňování" }, "websiteUri": { - "message": "Website (URI)" + "message": "Webová stránka (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Webová stránka přidána" }, "addWebsite": { - "message": "Add website" + "message": "Přidat webovou stránku" }, "deleteWebsite": { - "message": "Delete website" + "message": "Vymazat webovou stránku" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Výchozí ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Zobrazit detekci shody $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Skrýt detekci shody $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Automaticky vyplnit při načtení stránky?" }, "cardDetails": { "message": "Podrobnosti karty" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Přiřadit" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Umístění položky" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden má nový vzhled!" }, @@ -4098,6 +4163,12 @@ "message": "Je snazší a intuitivnější než kdy jindy automaticky vyplňovat a vyhledávat z karty trezor. Mrkněte se!" }, "accountActions": { - "message": "Account actions" + "message": "Činnosti účtu" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 12df0cb8fa4..857dcc13642 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Golygu ffolder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Dileu'r ffolder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 2fc1a8c8013..22032050711 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Redigér mappe" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Slet mappe" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Én eller flere organisationspolitikker påvirker dine generatorindstillinger." }, + "passwordGenerator": { + "message": "Adgangskodegenerator" + }, + "usernameGenerator": { + "message": "Brugernavngenerator" + }, + "useThisPassword": { + "message": "Anvend denne adgangskode" + }, + "useThisUsername": { + "message": "Anvend dette brugernavn" + }, + "securePasswordGenerated": { + "message": "Sikker adgangskode genereret! Glem ikke at opdatere adgangskoden på webstedet også." + }, + "useGeneratorHelpTextPartOne": { + "message": "Anvend generatoren", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "til at oprette en stærk, unik adgangskode", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Boks timeout-handling" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Konto mismatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrisk oplåsning mislykkedes. Den hemmelige biometriske nøgle kunne ikke oplåse boksen. Prøv at opsætte biometri igen." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometrisk nøgle matcher ikke" + }, "biometricsNotEnabledTitle": { "message": "Biometri ikke aktiveret" }, @@ -2328,7 +2375,7 @@ "message": "Din hovedadgangskode overholder ikke en eller flere organisationspolitikker. For at få adgang til boksen skal hovedadgangskode opdateres nu. Fortsættes, logges du ud af den nuværende session og vil skulle logger ind igen. Aktive sessioner på andre enheder kan forblive aktive i op til én time." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Organisationen har deaktiveret betroet enhedskryptering. Opsæt en hovedadgangskode for at tilgå boksen." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatisk tilmelding" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Adgangsnøgler", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Adgangskoder", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log ind med adgangsnøgle", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Tildel" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Emneplacering" }, + "fileSends": { + "message": "Fil-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, "bitwardenNewLook": { "message": "Bitwarden har fået et nyt look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Kontohandlinger" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 9fe95936316..dc2e2d34bdd 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Ordner bearbeiten" }, + "newFolder": { + "message": "Neuer Ordner" + }, + "folderName": { + "message": "Ordnername" + }, + "folderHintText": { + "message": "Verschachtel einen Ordner, indem du den Namen des übergeordneten Ordners hinzufügst, gefolgt von einem „/“. Beispiel: Social/Foren" + }, + "noFoldersAdded": { + "message": "Keine Ordner hinzugefügt" + }, + "createFoldersToOrganize": { + "message": "Erstelle Ordner, um deine Tresor-Einträge zu organisieren" + }, + "deleteFolderPermanently": { + "message": "Bist du sicher, dass du diesen Ordner dauerhaft löschen willst?" + }, "deleteFolder": { "message": "Ordner löschen" }, @@ -1343,13 +1361,13 @@ "message": "Tresor in der Seitenleiste öffnen" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Die zuletzt verwendeten Zugangsdaten für die aktuelle Website automatisch ausfüllen" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Die zuletzt verwendete Karte für die aktuelle Website automatisch ausfüllen" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Die zuletzt verwendete Identität für die aktuelle Website automatisch ausfüllen" }, "commandGeneratePasswordDesc": { "message": "Ein neues zufälliges Passwort generieren und in die Zwischenablage kopieren" @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Basis-Domain (empfohlen)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Eine oder mehrere Organisationsrichtlinien beeinflussen deine Generator-Einstellungen." }, + "passwordGenerator": { + "message": "Passwort-Generator" + }, + "usernameGenerator": { + "message": "Benutzernamen-Generator" + }, + "useThisPassword": { + "message": "Dieses Passwort verwenden" + }, + "useThisUsername": { + "message": "Diesen Benutzernamen verwenden" + }, + "securePasswordGenerated": { + "message": "Sicheres Passwort generiert! Vergiss nicht, auch dein Passwort auf der Website zu aktualisieren." + }, + "useGeneratorHelpTextPartOne": { + "message": "Verwende den Generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": ", um ein starkes einzigartiges Passwort zu erstellen", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Aktion bei Tresor-Timeout" }, @@ -1842,7 +1883,7 @@ "message": "Bestätigung der Timeout-Aktion" }, "autoFillAndSave": { - "message": "Auto-Ausfüllen und speichern" + "message": "Automatisch ausfüllen und speichern" }, "fillAndSave": { "message": "Ausfüllen und speichern" @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Konten stimmen nicht überein" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrische Entsperrung fehlgeschlagen. Der biometrische Geheimschlüssel konnte den Tresor nicht entsperren. Bitte versuche Biometrie erneut einzurichten." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometrische Schlüssel stimmen nicht überein" + }, "biometricsNotEnabledTitle": { "message": "Biometrie ist nicht eingerichtet" }, @@ -2328,7 +2375,7 @@ "message": "Dein Master-Passwort entspricht nicht einer oder mehreren Richtlinien deiner Organisation. Um auf den Tresor zugreifen zu können, musst du dein Master-Passwort jetzt aktualisieren. Wenn du fortfährst, wirst du von deiner aktuellen Sitzung abgemeldet und musst dich erneut anmelden. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Deine Organisation hat die vertrauenswürdige Geräteverschlüsselung deaktiviert. Bitte lege ein Master-Passwort fest, um auf deinen Tresor zuzugreifen." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische Registrierung" @@ -2757,10 +2804,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Deine Organisationsrichtlinien haben Auto-Ausfüllen beim Laden von Seiten aktiviert." + "message": "Deine Organisationsrichtlinien haben das automatische Ausfüllen beim Laden der Seite aktiviert." }, "howToAutofill": { - "message": "So funktioniert Auto-Ausfüllen" + "message": "So funktioniert automatisches Ausfüllen" }, "autofillSelectInfoWithCommand": { "message": "Wähle einen Eintrag von dieser Bildschirmseite, verwende das Tastaturkürzel $COMMAND$ oder entdecke andere Optionen in den Einstellungen.", @@ -2787,16 +2834,16 @@ "message": "Tastenkombination ändern" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Tastaturkürzel verwalten" }, "autofillShortcut": { "message": "Auto-Ausfüllen Tastaturkürzel" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "Das Tastaturkürzel zum automatischen Ausfüllen von Zugangsdaten ist nicht festgelegt. Du kannst es in den Browser-Einstellungen ändern." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "Das Tastaturkürzel zum automatischen Ausfüllen von Zugangsdaten ist $COMMAND$. Verwalte alle Tastaturkürzel in den Browser-Einstellungen.", "placeholders": { "command": { "content": "$1", @@ -2805,7 +2852,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Standard-Auto-Ausfüllen Tastaturkürzel: $COMMAND$.", + "message": "Standard-Auto-Ausfüllen Tastenkombination: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -3831,22 +3878,22 @@ "message": "Authenticator-Schlüssel" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Auto-Ausfüllen Optionen" }, "websiteUri": { "message": "Website (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Website hinzugefügt" }, "addWebsite": { - "message": "Add website" + "message": "Website hinzufügen" }, "deleteWebsite": { - "message": "Delete website" + "message": "Website löschen" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Übereinstimmungs-Erkennung anzeigen $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Übereinstimmungs-Erkennung verstecken $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Auto-Ausfüllen beim Laden einer Seite?" }, "cardDetails": { "message": "Kartendetails" @@ -3897,6 +3944,18 @@ "data": { "message": "Daten" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwörter", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Mit Passkey anmelden", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Zuweisen" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Eintrags-Standort" }, + "fileSends": { + "message": "Datei-Sends" + }, + "textSends": { + "message": "Text-Sends" + }, "bitwardenNewLook": { "message": "Bitwarden hat einen neuen Look!" }, @@ -4098,6 +4163,12 @@ "message": "Auto-Ausfüllen und Suchen vom Tresor-Tab ist einfacher und intuitiver als je zuvor. Schau dich um!" }, "accountActions": { - "message": "Account actions" + "message": "Konto-Aktionen" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index b7393c6ec22..1e898f7e0bf 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Επεξεργασία φακέλου" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Διαγραφή φακέλου" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Μία ή περισσότερες πολιτικές του οργανισμού επηρεάζουν τις ρυθμίσεις της γεννήτριας." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Ενέργεια Χρόνου Λήξης Vault" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Απόρριψη λογαριασμού" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Το βιομετρικό ξεκλείδωμα απέτυχε. Το βιομετρικό μυστικό κλειδί απέτυχε να ξεκλειδώσει το θησαυ/κιο. Προσπαθήστε να ρυθμίσετε ξανά τα βιομετρικά." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Αναντιστοιχία βιομετρικού κλειδιού" + }, "biometricsNotEnabledTitle": { "message": "Δεν έχουν οριστεί βιομετρικά" }, @@ -2328,7 +2375,7 @@ "message": "Ο Κύριος κωδικός πρόσβασης δεν πληροί τις απαιτήσεις πολιτικής αυτού του οργανισμού. Για να έχετε πρόσβαση στο vault, πρέπει να ενημερώσετε τον Κύριο σας κωδικό άμεσα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Ο οργανισμός σας έχει απενεργοποιήσει την κρυπτογράφηση αξιόπιστης συσκευής. Παρακαλώ ορίστε έναν κύριο κωδικό πρόσβασης για να αποκτήσετε πρόσβαση στο θησαυροφυλάκιο σας." }, "resetPasswordPolicyAutoEnroll": { "message": "Αυτόματη εγγραφή" @@ -3897,6 +3944,18 @@ "data": { "message": "Δεδομένα" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Ανάθεση" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Τοποθεσία Αντικειμένου" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Το Bitwarden έχει μια νέα εμφάνιση!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Ενέργειες λογαριασμού" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index db1f960b9b3..c65535e2e50 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently":{ + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -672,6 +690,9 @@ "totpHelperWithCapture": { "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, "copyTOTP": { "message": "Copy Authenticator key (TOTP)" }, @@ -1803,6 +1824,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -3837,11 +3881,21 @@ "message": "Authenticator key" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Autofill options" }, "websiteUri": { "message": "Website (URI)" }, + "websiteUriCount": { + "message": "Website (URI) $COUNT$", + "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "websiteAdded": { "message": "Website added" }, @@ -3903,6 +3957,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4111,5 +4177,14 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Enterprise policy requirements have been applied to this setting" } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 4b875de6efb..624e715926f 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organise your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organisation policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account mismatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key mismatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -2328,7 +2375,7 @@ "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Your organisation has disabled trusted device encryption. Please set a master password to access your vault." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrolment" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index a0b5ddaa57e..65b347eacbc 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organisation policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key mismatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not enabled" }, @@ -2328,7 +2375,7 @@ "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Your organisation has disabled trusted device encryption. Please set a master password to access your vault." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic Enrollment" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 40bdc71f15a..fc501882ca0 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Editar carpeta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Eliminar carpeta" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una o más políticas de la organización están afectando la configuración del generador" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Acción de tiempo de espera de la caja fuerte" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Las cuentas son distintas" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometría deshabilitada" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 81fec065db2..bdceb79217e 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Muuda kausta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Kustuta Kaust" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Organisatsiooni seaded mõjutavad parooli genereerija sätteid." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Hoidla ajalõpu tegevus" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Kontod ei ühti" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biomeetria ei ole sisse lülitatud" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 87eb73b11a3..6dc87cff44c 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Editatu Karpeta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Ezabatu karpeta" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Erakundeko politika batek edo gehiagok sortzailearen konfigurazioari eragiten diote." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Kutxa gotorraren itxaronaldiaren ekintza" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Kontu ezberdinak dira" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometria desgaitua" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 7b56e6c09cb..cbbb6cff3e4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "ويرايش پوشه" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "حذف پوشه" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "یک یا چند سیاست سازمان بر تنظیمات تولید کننده شما تأثیر می‌گذارد." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "عمل متوقف شدن گاو‌صندوق" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "عدم مطابقت حساب کاربری" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "بیومتریک برپا نشده" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index c4944c3ce7a..0f5d9ff1623 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Muokkaa kansiota" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Poista kansio" }, @@ -1343,13 +1361,13 @@ "message": "Avaa holvi sivupalkissa" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty kirjautumistieto." }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty kortti." }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty henkilöllisyys." }, "commandGeneratePasswordDesc": { "message": "Luo uusi satunnainen salasana ja kopioi se leikepöydälle." @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Juuriverkkotunnus (suositus)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Yksi tai useampi organisaatiokäytäntö vaikuttaa generaattorisi asetuksiin." }, + "passwordGenerator": { + "message": "Salasanageneraattori" + }, + "usernameGenerator": { + "message": "Käyttäjätunnusgeneraattori" + }, + "useThisPassword": { + "message": "Käytä tätä salasanaa" + }, + "useThisUsername": { + "message": "Käytä tätä käyttäjätunnusta" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Käytä generaattoria", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Holvin aikakatkaisutoiminto" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Tili ei täsmää" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrinen avaus epäonnistui. Biometrinen salainen avain ei voinut avata holvia. Yritä määrittää biometria uudelleen." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometrinen avain ei täsmää" + }, "biometricsNotEnabledTitle": { "message": "Biometriaa ei ole määritetty" }, @@ -2328,7 +2375,7 @@ "message": "Pääsalasanasi ei täytä yhden tai useamman organisaatiokäytännön vaatimuksia ja holvin käyttämiseksi sinun on vaihdettava se nyt. Tämä uloskirjaa kaikki nykyiset istunnot pakottaen uudelleenkirjautumisen. Muiden laitteiden aktiiviset istunnot saattavat toimia vielä tunnin ajan." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Organisaatiosi on estänyt luotettavan laitesalauksen. Käytä holviasi asettamalla pääsalasana." }, "resetPasswordPolicyAutoEnroll": { "message": "Automaattinen liitos" @@ -2787,16 +2834,16 @@ "message": "Muuta pikanäppäintä" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Hallitse pikanäppäimiä" }, "autofillShortcut": { "message": "Automaattitäytön pikanäppäin" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "Automaattitäytön pikanäppäintä ei ole määritetty. Määritä se selaimen asetuksista." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "Kirjautumistiedon automaatttitäytön pikanäppäin on $COMMAND$. Hallitse pikanäppäimiä selaimen asetuksista.", "placeholders": { "command": { "content": "$1", @@ -3831,22 +3878,22 @@ "message": "Todennusavain" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Automaattitäytön asetukset" }, "websiteUri": { - "message": "Website (URI)" + "message": "Verkkosivusto (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Verkkosivusto lisättiin" }, "addWebsite": { - "message": "Add website" + "message": "Lisää verkkosivusto" }, "deleteWebsite": { - "message": "Delete website" + "message": "Poista verkkosivusto" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Oletus ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Näytä vastaavuuden tunnistus $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Piilota vastaavuuden tunnistus $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Automaattitäytetäänkö sivun avautuessa?" }, "cardDetails": { "message": "Kortin tiedot" @@ -3897,6 +3944,18 @@ "data": { "message": "Tiedot" }, + "passkeys": { + "message": "Avainkoodit", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Salasanat", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Kirjaudu avainkoodilla", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Määritä" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Kohteen sijainti" }, + "fileSends": { + "message": "Tiedosto-Sendit" + }, + "textSends": { + "message": "Teksti-Sendit" + }, "bitwardenNewLook": { "message": "Bitwardenilla on uusi ulkoasu!" }, @@ -4098,6 +4163,12 @@ "message": "Automaattitäyttäminen ja hakujen tekeminen Holvi-välilehdeltä on entistä helpompaa ja intuitiivisempaa. Kokeile nyt!" }, "accountActions": { - "message": "Account actions" + "message": "Tilitoiminnot" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 9ed5ae7f85f..ee909343087 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "I-edit ang folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Burahin ang folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Isang o higit pang patakaran ng organisasyon ay nakakaapekto sa iyong mga setting ng generator." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Aksyon sa Vault timeout" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Mismatch sa Account" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Hindi naka-setup ang biometrics" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 47aec7928d3..ace9915f1d7 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Modifier le dossier" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Supprimer le dossier" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Une ou plusieurs politiques de sécurité de l'organisation affectent les paramètres de votre générateur." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Action après délai d'expiration du coffre" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Erreur de correspondance entre les comptes" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Le déverrouillage biométrique n'est pas activé" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 1ab856e8247..59e10828b19 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Editar cartafol" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Eliminar cartafol" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 0a0fab1e1c9..498f46447da 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "ערוך תיקייה" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "מחק תיקייה" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "מדיניות ארגונית אחת או יותר משפיעה על הגדרות המחולל שלך." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "פעולה לביצוע בכספת בתום זמן החיבור" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "חוסר התאמה בין חשבונות" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "אמצעי זיהוי ביומטרים לא מאופשרים" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 281e3bc011c..5dce5f7ff82 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit Folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete Folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "एक या एक से अधिक संगठन नीतियां आपकी जनरेटर सेटिंग को प्रभावित कर रही हैं।" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "वॉल्ट मध्यांतर कार्रवाई" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "खाता गलत मैच" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "बायोमेट्रिक अनलॉक विफल रहा. बायोमेट्रिक गुप्त कुंजी वॉल्ट को अनलॉक करने में विफल रही. कृपया बायोमेट्रिक्स को फिर से सेट करने का प्रयास करें." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "बेमेल बायोमेट्रिक कुंजी" + }, "biometricsNotEnabledTitle": { "message": "बॉयोमीट्रिक्स सक्षम नहीं है" }, @@ -2328,7 +2375,7 @@ "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "आपके संगठन ने विश्वसनीय डिवाइस एन्क्रिप्शन अक्षम कर दिया है. कृपया अपने वॉल्ट तक पहुँचने के लिए मास्टर पासवर्ड सेट करें." }, "resetPasswordPolicyAutoEnroll": { "message": "स्वचालित नामांकन" @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "बिटवार्डन का नया रूप!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index b2f2366fab7..fb61827321c 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -135,13 +135,13 @@ "message": "Auto-ispuna" }, "autoFillLogin": { - "message": "Automatsko popunjavanje prijave" + "message": "Auto-ispuna prijave" }, "autoFillCard": { - "message": "Automatsko popunjavanje kartice" + "message": "Auto-ispuna kartice" }, "autoFillIdentity": { - "message": "Automatsko popunjavanje identiteta" + "message": "Auto-ispuna identiteta" }, "generatePasswordCopied": { "message": "Generiraj lozinku (i kopiraj)" @@ -304,6 +304,24 @@ "editFolder": { "message": "Uredi mapu" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Izbriši mapu" }, @@ -652,7 +670,7 @@ } }, "autofillError": { - "message": "Nije moguće automatski ispuniti odabranu stavku na ovoj stranici. Umjesto toga kopirajte i zalijepite podatke." + "message": "Nije moguće auto-ispuniti odabranu prijavu na ovoj stranici. Umjesto toga kopiraj/zalijepi podatke." }, "totpCaptureError": { "message": "Nije moguće skenirati QR kod s trenutne web stranice" @@ -867,7 +885,7 @@ "message": "Ažuriraj" }, "notificationUnlockDesc": { - "message": "Za dovršetak auto-ispune, otključaj svoj trezor." + "message": "Za dovršetak auto-ispune, otključaj svoj Bitwarden trezor." }, "notificationUnlock": { "message": "Otključaj" @@ -1104,7 +1122,7 @@ "message": "Automatski kopiraj TOTP" }, "disableAutoTotpCopyDesc": { - "message": "Ako se za prijavu koristi dvostruka autentifikacija, TOTP kontrolni kôd se automatski kopira u međuspremnik nakon auto-ispune korisničkog imena i lozinke." + "message": "Ako za prijavu postoji autentifikatorski ključ, kopiraj TOTP kontrolni kôd u međuspremnik nakon auto-ispune prijave." }, "enableAutoBiometricsPrompt": { "message": "Traži biometrijsku autentifikaciju pri pokretanju" @@ -1489,7 +1507,7 @@ "message": "dr." }, "mx": { - "message": "Mx" + "message": "gx." }, "firstName": { "message": "Ime" @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Osnovna domena (preporučeno)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Jedno ili više pravila organizacije utječe na postavke generatora." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Nakon isteka trezora" }, @@ -1848,10 +1889,10 @@ "message": "Ispuni i spremi" }, "autoFillSuccessAndSavedUri": { - "message": "Auto-ispunjena stavka i spremanje URI" + "message": "Stavka auto-ispunjena i spremljen URI" }, "autoFillSuccess": { - "message": "Stavka je automatski popunjena" + "message": "Stavka je auto-ispunjena " }, "insecurePageWarning": { "message": "Upozorenje: Ovo je nezaštićena HTTP stranica i svi podaci koje preko nje pošalješ drugi mogu vidjeti i izmijeniti. Ova prijava je prvotno bila spremljena za sigurnu (HTTPS) stranicu." @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Pogrešan korisnički račun" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrijsko otključavanje nije uspjelo. Biometrijski tajni ključ nije uspio otključati trezor. Pokušaj ponovo postaviti biometriju." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Neusklađenost biometrijskog ključa" + }, "biometricsNotEnabledTitle": { "message": "Biometrija nije omogućena" }, @@ -2328,7 +2375,7 @@ "message": "Tvoja glavna lozinka ne zadovoljava pravila ove organizacije. Za pristup trezoru moraš odmah ažurirati svoju glavnu lozinku. Ako nastaviš, odjaviti ćeš se iz trenutne sesije te ćeš se morati ponovno prijaviti. Aktivne sesije na drugim uređajima mogu ostati aktivne do jedan sat." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Tvoja je organizacija onemogućila šifriranje pouzdanog uređaja. Postavi glavnu lozinku za pristup svom trezoru." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatsko učlanjenje" @@ -3831,22 +3878,22 @@ "message": "Kôd za provjeru" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Postavke auto-ispune" }, "websiteUri": { - "message": "Website (URI)" + "message": "Web stranica (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Web stranica dodana" }, "addWebsite": { - "message": "Add website" + "message": "Dodaj web stranicu" }, "deleteWebsite": { - "message": "Delete website" + "message": "Izbriši web stranicu" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Zadano ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Prikaži otkrivanje podudaranja $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Sakrij otkrivanje podudaranja $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Auto-ispuna kod učitavanja?" }, "cardDetails": { "message": "Detalji kartice" @@ -3897,6 +3944,18 @@ "data": { "message": "Podaci" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Dodijeli" }, @@ -3937,7 +3996,7 @@ "message": "Koristi skrivena polja za osjetljive podatke poput lozinke" }, "checkBoxHelpText": { - "message": "Koristi potvrdne okvire ako ih želiš automatski uključiti u obrascu, npr. zapamti adresu e-pošte" + "message": "Koristi potvrdne okvire ako ih želiš auto-ispuniti u obrascu, npr. zapamti adresu e-pošte" }, "linkedHelpText": { "message": "Koristi povezano polje kada imaš problema s auto-ispunom za određenu web stranicu." @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Lokacija stavke" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden ima novi izgled!" }, @@ -4098,6 +4163,12 @@ "message": "Auto-ispuna i pretraga iz kartice Trezor je lakša i intuitivnija nego ikad prije. Razgledaj!" }, "accountActions": { - "message": "Account actions" + "message": "Radnje na računu" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e17ad24130c..ecd5284d2ab 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Mappa szerkesztése" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Mappa törlése" }, @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Alap domain (ajánlott)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Egy vagy több szervezeti szabály érinti a generátor beállításokat." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Széf időkifutás művelet" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "A fiók nem egyezik." }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "A biometrikus adatok nincsenek beüzemelve." }, @@ -2328,7 +2375,7 @@ "message": "A mesterjelszó nem felel meg egy vagy több szervezeti szabályzatnak. A széf eléréséhez frissíteni kell a meszerjelszót. A továbblépés kijelentkeztet az aktuális munkamenetből és újra be kell jelentkezni. A többi eszközön lévő aktív munkamenetek akár egy óráig is aktívak maradhatnak." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "A szervezete letiltotta a megbízható eszközök titkosítását. Állítsunk be egy mesterjelszót a széf eléréséhez." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatikus regisztráció" @@ -3831,22 +3878,22 @@ "message": "Hitelesítő kulcs" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Automatikus kitöltés opciók" }, "websiteUri": { - "message": "Website (URI)" + "message": "Webhely (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "A webhely hozzáadásra került." }, "addWebsite": { - "message": "Add website" + "message": "Webhely hozzáadása" }, "deleteWebsite": { - "message": "Delete website" + "message": "Webhely törlése" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Alapértelmezés ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "$WEBSITE$ egyező érzékelés megjelenítése", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "$WEBSITE$ egyező érzékelés elrejtése", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Automatikus kitöltés oldalbetöltésnél?" }, "cardDetails": { "message": "Kártyaadatok" @@ -3897,6 +3944,18 @@ "data": { "message": "Adat" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Elem helyek" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4098,6 +4163,12 @@ "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" }, "accountActions": { - "message": "Account actions" + "message": "Fiókműveletek" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b73ac29fa87..05dc4ed1a7b 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Sunting Folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Hapus Folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Satu atau lebih kebijakan organisasi mempengaruhi pengaturan pembuat sandi Anda." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Tindakan Batas Waktu Brankas" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Akun tidak cocok" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrik tidak diaktifkan" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index b4a0468cfaf..447cdcae573 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Modifica cartella" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Elimina cartella" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una o più politiche dell'organizzazione stanno influenzando le impostazioni del tuo generatore." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Azione timeout cassaforte" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account non corrispondono" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Autenticazione biometrica non abilitata" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 6fe2f080a4a..3781e3afbd0 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "フォルダーを編集" }, + "newFolder": { + "message": "新しいフォルダー" + }, + "folderName": { + "message": "フォルダー名" + }, + "folderHintText": { + "message": "親フォルダーの名前の後に「/」を追加するとフォルダをネストします。例: ソーシャル/フォーラム" + }, + "noFoldersAdded": { + "message": "フォルダーが追加されていません" + }, + "createFoldersToOrganize": { + "message": "保管庫のアイテムを整理するフォルダーを作成します" + }, + "deleteFolderPermanently": { + "message": "このフォルダーを完全に削除しますか?" + }, "deleteFolder": { "message": "フォルダーを削除" }, @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "ベースドメイン (推奨)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "一つ以上の組織のポリシーがパスワード生成の設定に影響しています。" }, + "passwordGenerator": { + "message": "パスワード生成ツール" + }, + "usernameGenerator": { + "message": "ユーザー名生成ツール" + }, + "useThisPassword": { + "message": "このパスワードを使用する" + }, + "useThisUsername": { + "message": "このユーザー名を使用する" + }, + "securePasswordGenerated": { + "message": "安全なパスワードを生成しました! ウェブサイト上でパスワードを更新することを忘れないでください。" + }, + "useGeneratorHelpTextPartOne": { + "message": "生成機能", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "を使うと強力で一意なパスワードを作れます", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "保管庫タイムアウト時のアクション" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "アカウントが一致しません" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "生体認証のロック解除に失敗しました。生体認証キーでの保管庫のロック解除に失敗しました。生体認証を再度設定してください。" + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "生体認証キーが一致しません" + }, "biometricsNotEnabledTitle": { "message": "生体認証が有効になっていません" }, @@ -2328,7 +2375,7 @@ "message": "あなたのマスターパスワードは、組織のポリシーを満たしていません。保管庫にアクセスするには、今すぐマスターパスワードを更新する必要があります。この操作を続けると、現在のセッションがログアウトされ、再ログインする必要があります。他のデバイスでのアクティブなセッションは最大1時間継続する場合があります。" }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "あなたの組織は信頼できるデバイスの暗号化を無効化しました。保管庫にアクセスするにはマスターパスワードを設定してください。" }, "resetPasswordPolicyAutoEnroll": { "message": "自動登録" @@ -3831,22 +3878,22 @@ "message": "認証キー" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "自動入力オプション" }, "websiteUri": { - "message": "Website (URI)" + "message": "ウェブサイト (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "ウェブサイトを追加しました" }, "addWebsite": { - "message": "Add website" + "message": "ウェブサイトを追加" }, "deleteWebsite": { - "message": "Delete website" + "message": "ウェブサイトを削除" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "デフォルト ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "一致検出 $WEBSITE$を表示", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "一致検出 $WEBSITE$を非表示", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "ページ読み込み時に自動入力する" }, "cardDetails": { "message": "カード情報" @@ -3897,6 +3944,18 @@ "data": { "message": "データ" }, + "passkeys": { + "message": "パスキー", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "パスワード", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "パスキーでログイン", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "割り当て" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "アイテムの場所" }, + "fileSends": { + "message": "ファイル Send" + }, + "textSends": { + "message": "テキスト Send" + }, "bitwardenNewLook": { "message": "Bitwarden が新しい外観になりました。" }, @@ -4098,6 +4163,12 @@ "message": "保管庫タブからの自動入力と検索がこれまで以上に簡単で直感的になりました。" }, "accountActions": { - "message": "Account actions" + "message": "アカウントの操作" + }, + "showNumberOfAutofillSuggestions": { + "message": "拡張機能アイコンにログイン自動入力の候補の数を表示する" + }, + "systemDefault": { + "message": "システムのデフォルト" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index e520ca8ea10..898742626b3 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "საქაღალდის რედაქტირება" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "საქაღალდის წაშლა" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 50a23b4de23..49c37c0cf5b 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "ಫೋಲ್ಡರ್ ಸಂಪಾದಿಸಿ" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "ಫೋಲ್ಡರ್ ಅಳಿಸಿ" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆ ನೀತಿಗಳು ನಿಮ್ಮ ಜನರೇಟರ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುತ್ತವೆ" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "ವಾಲ್ಟ್ ಸಮಯ ಮೀರುವ ಕ್ರಿಯೆ" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "ಖಾತೆ ಹೊಂದಿಕೆಯಾಗುವುದಿಲ್ಲ" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "ಬಯೊಮಿಟ್ರಿಕ್ಸ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿಲ್ಲ" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index ac48c83e2a1..bd94d590c2f 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -117,19 +117,19 @@ "message": "보안 코드 복사" }, "copyName": { - "message": "Copy name" + "message": "이름 복사" }, "copyCompany": { - "message": "Copy company" + "message": "회사 복사" }, "copySSN": { - "message": "Copy Social Security number" + "message": "주민등록번호 복사" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "여권 번호 복사" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "운전면허 번호 복사" }, "autoFill": { "message": "자동 완성" @@ -257,7 +257,7 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "bitwarden.com 으로 이동할까요?" }, "bitwardenForBusiness": { "message": "Bitwarden for Business" @@ -304,6 +304,24 @@ "editFolder": { "message": "폴더 편집" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "폴더 삭제" }, @@ -685,13 +703,13 @@ "message": "로그인 세션이 만료되었습니다." }, "logIn": { - "message": "Log in" + "message": "로그인" }, "restartRegistration": { "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "만료된 링크" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -761,7 +779,7 @@ "message": "새 URI" }, "addDomain": { - "message": "Add domain", + "message": "도메인 추가", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "하나 이상의 단체 정책이 생성기 규칙에 영항을 미치고 있습니다." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "보관함 시간 제한 초과시 동작" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "계정이 일치하지 않음" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "생체 인식이 활성화되지 않음" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 834658c4572..13d0652f47c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Redaguoti aplankalą" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Šalinti aplankalą" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Viena ar daugiau organizacijos politikų turi įtakos Jūsų generatoriaus nustatymams." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault skirtojo laiko veiksmas" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Paskyros neatitikimas" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Trūksta biometrinių duomenų nustatymų" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 7712ff1d179..f954248d30e 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Labot mapi" }, + "newFolder": { + "message": "Jauna mape" + }, + "folderName": { + "message": "Mapes nosaukums" + }, + "folderHintText": { + "message": "Apakšmapes var izveidot, ja pievieno iekļaujošās mapes nosaukumu, aiz kura ir \"/\". Piemēram: Tīklošanās/Forumi" + }, + "noFoldersAdded": { + "message": "Nav pievienota neviena mape" + }, + "createFoldersToOrganize": { + "message": "Mapes ir izveidojamas, lai sakārtotu savas glabātavas vienumus" + }, + "deleteFolderPermanently": { + "message": "Vai tiešām neatgriezeniski izdzēst šo mapi?" + }, "deleteFolder": { "message": "Dzēst mapi" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Viens vai vairāki apvienības nosacījumi ietekmē veidotāja iestatījumus." }, + "passwordGenerator": { + "message": "Paroļu veidotājs" + }, + "usernameGenerator": { + "message": "Lietotājvārdu veidotājs" + }, + "useThisPassword": { + "message": "Izmantot šo paroli" + }, + "useThisUsername": { + "message": "Izmantot šo lietotājvārdu" + }, + "securePasswordGenerated": { + "message": "Droša parole izveidota. Neaizmirsti arī atjaunināt savu paroli tīmekļvietnē!" + }, + "useGeneratorHelpTextPartOne": { + "message": "Veidotājs ir izmantojams", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": ", lai izveidotu spēcīgu un vienreizēju paroli", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Glabātavas noildzes darbība" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Konta nesaderība" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometriskā atslēgšana neizdevās. Biometriskā slepenā atslēgai neizdevās atslēgt glabātavu. Lūgums vēlreiz mēģināt iestatīt biometriju." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometriskās atslēgas neatbilstība" + }, "biometricsNotEnabledTitle": { "message": "Biometrija nav iespējota" }, @@ -2328,7 +2375,7 @@ "message": "Galvenā parole neatbilst vienam vai vairākiem apvienības nosacījumiem. Ir jāatjaunina galvenā parole, lai varētu piekļūt glabātavai. Turpinot notiks atteikšanās no pašreizējās sesijas, un būs nepieciešams pieteikties no jauna. Citās ierīcēs esošās sesijas var turpināt darboties līdz vienai stundai." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Tava apvienība ir atspējojusi uzticamo ierīču šifrēšanu. Lūgums iestatīt galveno paroli, lai piekļūtu savai glabātavai." }, "resetPasswordPolicyAutoEnroll": { "message": "Automātiska ievietošana sarakstā" @@ -3897,6 +3944,18 @@ "data": { "message": "Dati" }, + "passkeys": { + "message": "Piekļuves atslēgas", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Paroles", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Pieteikties ar piekļuves atslēgu", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Vienuma atrašanās vieta" }, + "fileSends": { + "message": "Datņu Send" + }, + "textSends": { + "message": "Teksta Send" + }, "bitwardenNewLook": { "message": "Bitwarden ir jauns izskats." }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Konta darbības" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index c102e4a7b0d..db36de26433 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "ഫോൾഡർ തിരുത്തുക" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "ഫോൾഡർ ഇല്ലാതാക്കുക" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "ഒന്നോ അതിലധികമോ സംഘടന നയങ്ങൾ നിങ്ങളുടെ പാസ്സ്‌വേഡ് സൃഷ്ടാവിൻ്റെ ക്രമീകരണങ്ങളെ ബാധിക്കുന്നു" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "വാൾട് ടൈം ഔട്ട് ആക്ഷൻ" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 32644cf9da5..f415cf6d332 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "फोल्डर संपादित करा" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "फोल्डर खोडून टाका" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 0e861ed319d..69e17a7d6d5 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Rediger mappen" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Slett mappen" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "En eller flere av virksomhetens regler påvirker generatorinnstillingene dine." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Handling ved tidsavbrudd i hvelvet" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Kontoen eksisterer ikke" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometri ikke aktivert" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index c50355f08aa..bff29a5e01e 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Map bewerken" }, + "newFolder": { + "message": "Nieuwe map" + }, + "folderName": { + "message": "Mapnaam" + }, + "folderHintText": { + "message": "Je kunt een map onderbrengen door het toevoegen van de naam van de bovenliggende map gevolgd door een \"/\". Voorbeeld: Social/Forums" + }, + "noFoldersAdded": { + "message": "Geen mappen toegevoegd" + }, + "createFoldersToOrganize": { + "message": "Maak mappen om je kluis items te organiseren" + }, + "deleteFolderPermanently": { + "message": "Weet je zeker dat je deze map definitief wilt verwijderen?" + }, "deleteFolder": { "message": "Map verwijderen" }, @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Basisdomein (aanbevolen)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Een of meer organisatiebeleidseisen heeft invloed op de instellingen van je generator." }, + "passwordGenerator": { + "message": "Wachtwoordgenerator" + }, + "usernameGenerator": { + "message": "Gebruikersnaamgenerator" + }, + "useThisPassword": { + "message": "Dit wachtwoord gebruiken" + }, + "useThisUsername": { + "message": "Deze gebruikersnaam gebruiken" + }, + "securePasswordGenerated": { + "message": "Veilig wachtwoord aangemaakt! Vergeet niet om je wachtwoord ook op de website bij te werken." + }, + "useGeneratorHelpTextPartOne": { + "message": "Gebruik de generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "om een sterk uniek wachtwoord te maken", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Actie bij time-out" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Accounts komt niet overeen" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrische ontgrendelen mislukt. De biometrische geheime sleutel kon de kluis niet ontgrendelen. Probeer biometrische gegevens opnieuw in te stellen." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometrische sleutel discrepantie" + }, "biometricsNotEnabledTitle": { "message": "Biometrie niet ingeschakeld" }, @@ -2328,7 +2375,7 @@ "message": "Je hoofdwachtwoord voldoet niet aan en of meerdere oganisatiebeleidsonderdelen. Om toegang te krijgen tot de kluis, moet je je hoofdwachtwoord nu bijwerken. Doorgaan zal je huidige sessie uitloggen, waarna je opnieuw moet inloggen. Actieve sessies op andere apparaten blijven mogelijk nog een uur actief." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Je organisatie heeft het versleutelen van vertrouwde apparaten uitgeschakeld. Stel een hoofdwachtwoord in om toegang te krijgen tot je kluis." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische inschrijving" @@ -3673,7 +3720,7 @@ "message": "Notifications" }, "appearance": { - "message": "Voorkomen" + "message": "Uiterlijk" }, "errorAssigningTargetCollection": { "message": "Fout bij toewijzen doelverzameling." @@ -3897,6 +3944,18 @@ "data": { "message": "Gegevens" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Wachtwoorden", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Inloggen met passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Toewijzen" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Itemlocatie" }, + "fileSends": { + "message": "Bestand-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, "bitwardenNewLook": { "message": "Bitwarden heeft een nieuw uiterlijk!" }, @@ -4098,6 +4163,12 @@ "message": "Automatisch invullen en zoeken is makkelijker en intuïtiever dan ooit vanaf het tabblad Kluis. Kijk rond!" }, "accountActions": { - "message": "Account actions" + "message": "Accountacties" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 6d8055ff5f6..8ee343b65d1 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edytuj folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Usuń folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Co najmniej jedna zasada organizacji wpływa na ustawienia generatora." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Sposób blokowania sejfu" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Konto jest niezgodne" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Odblokowanie biometryczne nie powiodło się. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Klucz biometryczny jest niepoprawny" + }, "biometricsNotEnabledTitle": { "message": "Dane biometryczne są wyłączone" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Dane" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Przypisz" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Lokalizacja elementu" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden ma nowy wygląd!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index bbee456fe54..69ed6479536 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -117,19 +117,19 @@ "message": "Copiar Código de Segurança" }, "copyName": { - "message": "Copy name" + "message": "Copiar nome" }, "copyCompany": { - "message": "Copy company" + "message": "Copiar empresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Cadastro de Pessoas Físicas" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copiar número do passaporte" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copiar número da CNH" }, "autoFill": { "message": "Autopreencher" @@ -304,6 +304,24 @@ "editFolder": { "message": "Editar Pasta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Excluir Pasta" }, @@ -814,7 +832,7 @@ "message": "Pedir para adicionar um item se um não for encontrado no seu cofre. Aplica-se a todas as contas logadas." }, "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "message": "Mostrar cartões como sugestões de preenchimento automático na exibição do Cofre" }, "showCardsCurrentTab": { "message": "Mostrar cartões em páginas com guias." @@ -823,7 +841,7 @@ "message": "Exibir itens de cartão em páginas com abas para simplificar o preenchimento automático" }, "showIdentitiesInVaultView": { - "message": "Show identifies as Autofill suggestions on Vault view" + "message": "Mostrar identifica como sugestões de preenchimento automático na exibição do Cofre" }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" @@ -1258,16 +1276,16 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Sugestões de preenchimento automático" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostrar sugestões de preenchimento automático nos campos de formulários" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Exibir sugestões quando o ícone for selecionado" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "Aplica-se a todas as contas conectadas." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desative as configurações do gerenciador de senhas do seu navegador para evitar conflitos." @@ -1288,7 +1306,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Preenchimento automático ao carregar a página" }, "enableAutoFillOnPageLoad": { "message": "Ativar o Autopreenchimento ao Carregar a Página" @@ -1297,7 +1315,7 @@ "message": "Se um formulário de login for detectado, realizar automaticamente um auto-preenchimento quando a página web carregar." }, "autofillOnPageLoadWarning": { - "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "message": "$OPENTAG$Aviso:$CLOSETAG$ Comprometido ou sites não confiáveis podem explorar o autopreenchimento ao carregar a página.", "placeholders": { "openTag": { "content": "$1", @@ -1313,7 +1331,7 @@ "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Saiba mais sobre riscos" }, "learnMoreAboutAutofill": { "message": "Saiba mais sobre preenchimento automático" @@ -1343,13 +1361,13 @@ "message": "Abrir cofre na barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Preencher automaticamente o último login utilizado para o site atual" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Preenchimento automático do último cartão utilizado para o site atual" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Autopreencher a última identidade usada para o site atual" }, "commandGeneratePasswordDesc": { "message": "Gerar e copiar uma nova senha aleatória para a área de transferência." @@ -1630,7 +1648,7 @@ "message": "Credenciais" }, "secureNotes": { - "message": "Notas Seguras" + "message": "Notas seguras" }, "clear": { "message": "Limpar", @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Domínio de base (recomendado)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1678,7 +1696,7 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Detecção de Correspondência", + "message": "Detecção de correspondência", "description": "URI match detection for autofill." }, "defaultMatchDetection": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Uma ou mais políticas da organização estão afetando as suas configurações do gerador." }, + "passwordGenerator": { + "message": "Gerador de Senha" + }, + "usernameGenerator": { + "message": "Gerador de usuário" + }, + "useThisPassword": { + "message": "Use esta senha" + }, + "useThisUsername": { + "message": "Use este nome de usuário" + }, + "securePasswordGenerated": { + "message": "Senha segura gerada! Não se esqueça de atualizar também sua senha no site." + }, + "useGeneratorHelpTextPartOne": { + "message": "Usar o gerador", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "para criar uma senha única e forte", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Ação de Tempo Limite do Cofre" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "A conta não confere" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "O desbloqueio biométrico falhou. A chave secreta biométrica não conseguiu desbloquear o cofre. Tente configurar os dados biométricos novamente." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Falta de chave biométrica" + }, "biometricsNotEnabledTitle": { "message": "Biometria não ativada" }, @@ -2115,7 +2162,7 @@ "message": "Protegido por senha" }, "copyLink": { - "message": "Copy link" + "message": "Copiar link" }, "copySendLink": { "message": "Copiar link do Send", @@ -2328,7 +2375,7 @@ "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Sua organização desativou a criptografia confiável do dispositivo. Por favor, defina uma senha mestra para acessar o seu cofre." }, "resetPasswordPolicyAutoEnroll": { "message": "Inscrição Automática" @@ -2784,19 +2831,19 @@ "message": "Autofill shortcut" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Alterar atalho" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Gerenciar atalhos" }, "autofillShortcut": { "message": "Atalho para autopreenchimento" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "O atalho de acesso ao preenchimento automático não está definido. Altere isso nas configurações do navegador." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "O atalho de login de preenchimento automático é $COMMAND$. Gerencie todos os atalhos nas configurações do navegador.", "placeholders": { "command": { "content": "$1", @@ -2980,10 +3027,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 campo precisa de sua atenção." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "Campos $COUNT$ precisam de sua atenção.", "placeholders": { "count": { "content": "$1", @@ -3092,7 +3139,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "Desbloqueie sua conta para ver as sugestões de preenchimento automático", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3100,7 +3147,7 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "Desbloqueie sua conta, abra em uma nova janela", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "fillCredentialsFor": { @@ -3124,27 +3171,27 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Novo login", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "Adicionar novo item de login no cofre, abre em uma nova janela", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Novo cartão", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "Adicione um novo item do cartão do cofre, abre em uma nova janela", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "Nova identidade", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "Adicionar novo item de identidade do cofre, abre em uma nova janela", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3264,7 +3311,7 @@ "message": "Senha do arquivo inválida, por favor informe a senha utilizada quando criou o arquivo de exportação." }, "destination": { - "message": "Destination" + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre suas opções de importação" @@ -3505,27 +3552,27 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Continuar nas configurações do navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Continuar para o Centro de Ajuda?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Alterar as configurações de autopreenchimento e gerenciamento de senhas do seu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Você pode ver e definir atalhos de extensão nas configurações do seu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Alterar as configurações de autopreenchimento e gerenciamento de senhas do seu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Você pode ver e definir atalhos de extensão nas configurações do seu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -3821,7 +3868,7 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "o número do cartão termina com", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { @@ -3831,22 +3878,22 @@ "message": "Chave do autenticador" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Opções de autopreenchimento" }, "websiteUri": { - "message": "Website (URI)" + "message": "Site (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Site adicionado" }, "addWebsite": { - "message": "Add website" + "message": "Adicionar site" }, "deleteWebsite": { - "message": "Delete website" + "message": "Excluir site" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Padrão ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Exibir detecção de correspondência $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Ocultar detecção de correspondência $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Preenchimento automático ao carregar a página?" }, "cardDetails": { "message": "Detalhes do cartão" @@ -3895,16 +3942,28 @@ "message": "Carregando" }, "data": { - "message": "Data" + "message": "Dado" + }, + "passkeys": { + "message": "Senhas", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Senhas", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Iniciar sessão com a chave de acesso", + "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { "message": "Atribuir" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "Apenas membros da organização com acesso a essas coleções poderão ver o item." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Apenas membros da organização com acesso à essas coleções poderão ver os itens." }, "bulkCollectionAssignmentWarning": { "message": "Você selecionou $TOTAL_COUNT$ itens. Você não pode atualizar $READONLY_COUNT$ destes itens porque você não tem permissão de edição.", @@ -3943,13 +4002,13 @@ "message": "Use um campo vinculado quando estiver enfrentando problemas com o auto-preenchimento com um site específico." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "Digite o Id html do campo, nome, nome aria-label, ou espaço reservado." }, "editField": { - "message": "Edit field" + "message": "Editar campo" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "Editar $LABEL$", "placeholders": { "label": { "content": "$1", @@ -3958,7 +4017,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Excluir $LABEL$", "placeholders": { "label": { "content": "$1", @@ -3967,7 +4026,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ adicionado", "placeholders": { "label": { "content": "$1", @@ -3976,7 +4035,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "Reordene $LABEL$. Use a tecla de seta para mover o item para cima ou para baixo.", "placeholders": { "label": { "content": "$1", @@ -3985,7 +4044,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "$LABEL$ se moveu para cima, posição $INDEX$ de $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4002,13 +4061,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Selecione as coleções para atribuir" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 item será transferido permanentemente para a organização selecionada. Você não irá mais possuir este item." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "Itens $PERSONAL_ITEMS_COUNT$ serão transferidos permanentemente para a organização selecionada. Você não irá mais possuir esses itens.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4017,7 +4076,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1 item será transferido permanentemente para $ORG$. Você não irá mais possuir este item.", "placeholders": { "org": { "content": "$1", @@ -4026,7 +4085,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "Os itens $PERSONAL_ITEMS_COUNT$ serão transferidos permanentemente para $ORG$. Você não irá mais possuir esses itens.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4039,13 +4098,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Coleções atribuídas com sucesso" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "Você selecionou nada." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "Itens selecionados movidos para $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4054,7 +4113,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Itens movidos para $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4063,7 +4122,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Item movido para $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4072,7 +4131,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "$LABEL$ se moveu para baixo, posição $INDEX$ de $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4091,13 +4150,25 @@ "itemLocation": { "message": "Localização do Item" }, + "fileSends": { + "message": "Arquivos enviados" + }, + "textSends": { + "message": "Texto enviado" + }, "bitwardenNewLook": { - "message": "Bitwarden has a new look!" + "message": "Bitwarden tem uma nova aparência!" }, "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "É mais fácil e mais intuitivo do que nunca autopreenchimento e pesquise na guia Cofre. Dê uma olhada ao redor!" }, "accountActions": { - "message": "Account actions" + "message": "Ações da conta" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 6bdeb8160da..fc4ca1c3a5c 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Editar pasta" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Eliminar pasta" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Uma ou mais políticas da organização estão a afetar as suas definições do gerador." }, + "passwordGenerator": { + "message": "Gerador de palavras-passe" + }, + "usernameGenerator": { + "message": "Gerador de nomes de utilizador" + }, + "useThisPassword": { + "message": "Utilizar esta palavra-passe" + }, + "useThisUsername": { + "message": "Utilizar este nome de utilizador" + }, + "securePasswordGenerated": { + "message": "Palavra-passe segura gerada! Não se esqueça de atualizar também a sua palavra-passe no site." + }, + "useGeneratorHelpTextPartOne": { + "message": "Utilize o gerador", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "para criar uma palavra-passe forte e única", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Ação de tempo limite do cofre" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Incompatibilidade de contas" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "O desbloqueio biométrico falhou. A chave secreta biométrica não conseguiu desbloquear o cofre. Por favor, tente configurar a biometria novamente." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Incompatibilidade da chave biométrica" + }, "biometricsNotEnabledTitle": { "message": "Biometria não configurada" }, @@ -2328,7 +2375,7 @@ "message": "A sua palavra-passe mestra não cumpre uma ou mais políticas da sua organização. Para aceder ao cofre, tem de atualizar a sua palavra-passe mestra agora. Ao prosseguir, terminará a sua sessão atual e terá de iniciar sessão novamente. As sessões ativas noutros dispositivos poderão continuar ativas até uma hora." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "A sua organização desativou a encriptação de dispositivos fiáveis. Por favor, defina uma palavra-passe mestra para aceder ao seu cofre." }, "resetPasswordPolicyAutoEnroll": { "message": "Inscrição automática" @@ -3897,6 +3944,18 @@ "data": { "message": "Dados" }, + "passkeys": { + "message": "Chaves de acesso", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Palavras-passe", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Iniciar sessão com a chave de acesso", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Atribuir" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Localização do item" }, + "fileSends": { + "message": "Sends de ficheiros" + }, + "textSends": { + "message": "Sends de texto" + }, "bitwardenNewLook": { "message": "O Bitwarden tem um novo visual!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Ações da conta" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index b9a1d78fe50..c0a343477b5 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3,27 +3,27 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Manager Gratuit de Parole", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Acasă, la serviciu sau în deplasare, Bitwarden vă protejează toate parolele și informațiile sensibile", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Invitație acceptată" }, "createAccount": { "message": "Creare cont" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Setați o parolă puternică" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Finalizați crearea contului prin setarea unei parole" }, "login": { "message": "Conectare" @@ -53,7 +53,7 @@ "message": "Un indiciu pentru parola principală vă poate ajuta să v-o reamintiți dacă o uitați." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Dacă vă uitați parola, indiciul parolei poate fi trimis la adresa dvs. de e-mail. $CURRENT$/$MAXIMUM$ de caractere maxim.", "placeholders": { "current": { "content": "$1", @@ -72,10 +72,10 @@ "message": "Indiciu pentru parola principală (opțional)" }, "joinOrganization": { - "message": "Join organization" + "message": "Alăturați-vă organizației" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Finalizați aderarea la această organizație prin setarea unei parole principale." }, "tab": { "message": "Filă" @@ -117,31 +117,31 @@ "message": "Copiere cod de securitate" }, "copyName": { - "message": "Copy name" + "message": "Copiați numele" }, "copyCompany": { - "message": "Copy company" + "message": "Copiați firma" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Copiați numărul de securitate socială" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copiați numărul pașaportului" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copiați numărul de licență" }, "autoFill": { "message": "Auto-completare" }, "autoFillLogin": { - "message": "Autofill login" + "message": "Autocompletare date de autentificare" }, "autoFillCard": { - "message": "Autofill card" + "message": "Autocompletare card" }, "autoFillIdentity": { - "message": "Autofill identity" + "message": "Autocompletare identitate" }, "generatePasswordCopied": { "message": "Generare parolă (s-a copiat)" @@ -153,19 +153,19 @@ "message": "Nu există potrivire de autentificări" }, "noCards": { - "message": "No cards" + "message": "Niciun card" }, "noIdentities": { - "message": "No identities" + "message": "Nicio identitate" }, "addLoginMenu": { - "message": "Add login" + "message": "Adăugare date de autentificare" }, "addCardMenu": { - "message": "Add card" + "message": "Adăugare card" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Adăugare identitate" }, "unlockVaultMenu": { "message": "Deblocați-vă seiful" @@ -213,25 +213,25 @@ "message": "Schimbare parolă principală" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuați către aplicația web?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Explorați mai multe caracteristici ale contului Bitwarden în aplicația web." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Continuați la Centrul de Ajutor?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Aflați mai multe despre cum să utilizați Bitwarden în Centrul de Ajutor." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Continuați la magazinul de extensii al browser-ului?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Ajutați-i pe alții să afle dacă Bitwarden este potrivit pentru ei. Vizitați magazinul de extensii al browser-ului dvs. și lăsați o evaluare acum." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Puteți schimba parola principală în aplicația web Bitwarden." }, "fingerprintPhrase": { "message": "Fraza amprentă", @@ -248,43 +248,43 @@ "message": "Deconectare" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Despre Bitwarden" }, "about": { "message": "Despre" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Mai multe de la Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Continuați la bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden pentru Business" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Autentificator 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" + "message": "Autentificatorul Bitwarden vă permite să stocați chei de autentificare și să generați coduri TOTP pentru fluxurile de verificare în doi pași. Aflați mai multe de pe site-ul bitwarden.com" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Manager de secrete Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Stocați, gestionați și partajați în siguranță secretele dezvoltatorilor cu Bitwarden Secrets Manager. Aflați mai multe pe site-ul bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "Creați experiențe de conectare fluide și sigure, fără parole tradiționale, cu Passwordless.dev. Aflați mai multe pe site-ul bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Bitwarden Families gratuit" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Sunteți eligibil pentru Bitwarden Families gratuit. Răscumpărați această ofertă astăzi în aplicația web." }, "version": { "message": "Versiune" @@ -304,6 +304,24 @@ "editFolder": { "message": "Editare dosar" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Ștergere dosar" }, @@ -345,7 +363,7 @@ "message": "Generează automat parole unice și puternice pentru autentificările dvs." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Aplicația web Bitwarden" }, "importItems": { "message": "Import de articole" @@ -366,7 +384,7 @@ "message": "Lungime" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Lungimea minimă a parolei" }, "uppercase": { "message": "Litere mari (A-Z)" @@ -424,7 +442,7 @@ "message": "Parolă" }, "totp": { - "message": "Authenticator secret" + "message": "Cheie de autentificare" }, "passphrase": { "message": "Frază de acces" @@ -433,13 +451,13 @@ "message": "Favorit" }, "unfavorite": { - "message": "Unfavorite" + "message": "Elimină din favorite" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Item adăugat în favorite" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Item eliminat din favorite" }, "notes": { "message": "Note" @@ -463,7 +481,7 @@ "message": "Lansare" }, "launchWebsite": { - "message": "Launch website" + "message": "Lansați siteul web" }, "website": { "message": "Sait web" @@ -478,19 +496,19 @@ "message": "Altele" }, "unlockMethods": { - "message": "Unlock options" + "message": "Deblocați opțiunile" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Configurați metoda de deblocare care să schimbe acțiunea de expirare a seifului." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "Setați o metodă de deblocare in setări" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Expirarea sesiunii" }, "otherOptions": { - "message": "Other options" + "message": "Alte opțiuni" }, "rateExtension": { "message": "Evaluare extensie" @@ -533,7 +551,7 @@ "message": "Blocare imediată" }, "lockAll": { - "message": "Lock all" + "message": "Blochează toate" }, "immediately": { "message": "Imediat" @@ -581,16 +599,16 @@ "message": "Securitate" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Confirmați parola principală" }, "masterPassword": { - "message": "Master password" + "message": "Parola principală" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Parola principală nu poate fi recuperată dacă este uitată!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Indiciu pentru parola principală" }, "errorOccurred": { "message": "S-a produs o eroare" @@ -624,10 +642,10 @@ "message": "Noul dvs. cont a fost creat! Acum vă puteți autentifica." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "V-ați conectat cu succes" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Puteți închide această fereastră" }, "masterPassSent": { "message": "V-am trimis un e-mail cu indiciul parolei principale." @@ -636,7 +654,7 @@ "message": "Este necesar codul de verificare." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "Autentificarea a fost anulată sau a luat prea mult. Încercați din nou." }, "invalidVerificationCode": { "message": "Cod de verificare nevalid" @@ -655,49 +673,49 @@ "message": "Nu se pot auto-completa datele de conectare pentru această pagină. În schimb, puteți copia și lipi aceste date." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Nu se poate scana codul QR din pagina web curentă" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Cheie autentificare adăugată" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Scanează codul QR pentru autentificator din pagina web curentă" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Faceți autentificarea in 2 pași mai ușoară" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden poate stoca și completa coduri de verificare în doi pași. Copiați și lipiți cheia în acest câmp." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden poate stoca și completa coduri de verificare în doi pași. Selectați pictograma camerei foto pentru a face o captură de ecran a codului QR de autentificare al acestui site, sau copiați și lipiți cheia în acest câmp." }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Copiați cheia de autentificare (TOTP)" }, "loggedOut": { "message": "Deconectat" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Ați fost deconectat din contul dvs." }, "loginExpired": { "message": "Sesiunea de autentificare a expirat." }, "logIn": { - "message": "Log in" + "message": "Autentificare" }, "restartRegistration": { - "message": "Restart registration" + "message": "Reporniți înregistrarea" }, "expiredLink": { - "message": "Expired link" + "message": "Link expirat" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Vă rugăm să reporniți înregistrarea sau să încercați să vă conectați." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Este posibil să aveți deja un cont" }, "logOutConfirmation": { "message": "Sigur doriți să vă deconectați?" @@ -761,7 +779,7 @@ "message": "URI nou" }, "addDomain": { - "message": "Add domain", + "message": "Adăugați un domeniu", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -805,7 +823,7 @@ "message": "Solicitare de adăugare cont" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Salvare în opțiuni seif" }, "addLoginNotificationDesc": { "message": "Solicitați adăugarea unui element dacă nu se găsește unul în seif." @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una sau mai multe politici organizaționale vă afectează setările generatorului." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Acțiune la expirarea seifului" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Eroare de cont" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Datele biometrice nu sunt configurate" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 3dd4b5c4aa9..0890273f1ca 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Изменить папку" }, + "newFolder": { + "message": "Новый папка" + }, + "folderName": { + "message": "Название папки" + }, + "folderHintText": { + "message": "Создайте вложенную папку, добавив название родительской папки и символ \"/\". Пример: Сообщества/Форумы" + }, + "noFoldersAdded": { + "message": "Нет добавленных папок" + }, + "createFoldersToOrganize": { + "message": "Создавайте папки для упорядочивания элементов хранилища" + }, + "deleteFolderPermanently": { + "message": "Вы действительно хотите безвозвратно удалить эту папку?" + }, "deleteFolder": { "message": "Удалить папку" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "На настройки генератора влияют одна или несколько политик организации." }, + "passwordGenerator": { + "message": "Генератор паролей" + }, + "usernameGenerator": { + "message": "Генератор имени пользователя" + }, + "useThisPassword": { + "message": "Использовать этот пароль" + }, + "useThisUsername": { + "message": "Использовать это имя пользователя" + }, + "securePasswordGenerated": { + "message": "Безопасный пароль сгенерирован! Не забудьте также обновить свой пароль на сайте." + }, + "useGeneratorHelpTextPartOne": { + "message": "Использовать генератор", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "для создания надежного уникального пароля", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Действие по тайм-ауту хранилища" }, @@ -1848,7 +1889,7 @@ "message": "Заполнить и сохранить" }, "autoFillSuccessAndSavedUri": { - "message": "URI элемента заполнен и сохранен" + "message": "Элемент заполнен, URI сохранен" }, "autoFillSuccess": { "message": "Элемент заполнен " @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Несоответствие аккаунта" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Биометрическая разблокировка не удалась. Биометрический секретный ключ не смог разблокировать хранилище. Пожалуйста, попробуйте настроить биометрию еще раз." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Несовпадение биометрического ключа" + }, "biometricsNotEnabledTitle": { "message": "Биометрия не настроена" }, @@ -2328,7 +2375,7 @@ "message": "Ваш мастер-пароль не соответствует требованиям политики вашей организации. Для доступа к хранилищу вы должны обновить свой мастер-пароль прямо сейчас. При этом текущий сеанс будет завершен и потребуется повторная авторизация. Сеансы на других устройствах могут оставаться активными в течение часа." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "В вашей организации отключено шифрование доверенных устройств. Пожалуйста, установите мастер-пароль для доступа к вашему хранилищу." }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматическое развертывание" @@ -3897,6 +3944,18 @@ "data": { "message": "Данные" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Пароли", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Войти с passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Назначить" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Расположение элемента" }, + "fileSends": { + "message": "Файловая Send" + }, + "textSends": { + "message": "Текстовая Send" + }, "bitwardenNewLook": { "message": "У Bitwarden новый облик!" }, @@ -4098,6 +4163,12 @@ "message": "Теперь автозаполнение и поиск на вкладке Хранилище стали проще и интуитивно понятнее, чем когда-либо. Осмотритесь!" }, "accountActions": { - "message": "Account actions" + "message": "Действия с аккаунтом" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index d7495836ec4..fecfdfe8240 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "බහාලුම සංස්කරණය" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "ෆෝල්ඩරය මකන්න" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "සංවිධාන ප්රතිපත්ති එකක් හෝ වැඩි ගණනක් ඔබේ උත්පාදක සැකසුම් වලට බලපායි." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "සුරක්ෂිතාගාරය කාලය ක්රියාකාරී" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "ගිණුම මිස්ගැලච්" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "ජීව විද්යාව සක්රීය කර නැත" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 6acf4e0e442..21e48ce3ded 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Upraviť priečinok" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Odstrániť priečinok" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Jedno alebo viac nastavení organizácie ovplyvňujú vaše nastavenia generátora." }, + "passwordGenerator": { + "message": "Generátor hesla" + }, + "usernameGenerator": { + "message": "Generátor používateľského mena" + }, + "useThisPassword": { + "message": "Použiť toto heslo" + }, + "useThisUsername": { + "message": "Použit toto používateľské meno" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Akcia pri vypršaní času pre trezor" }, @@ -1983,7 +2024,7 @@ "message": "Aplikácia Bitwarden Desktop musí byť pred použitím odomknutia pomocou biometrických údajov spustená." }, "errorEnableBiometricTitle": { - "message": "Nie je môžné povoliť biometriu" + "message": "Nie je môžné povoliť biometrické údaje" }, "errorEnableBiometricDesc": { "message": "Akcia bola zrušená desktopovou aplikáciou" @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Nezhoda účtu" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Odomknutie biometrickými údajmi zlyhalo. Tajný kľúč biometrických údajov nedokázal odomknúť trezor. Skúste biometrické údaje nastaviť znova." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Kľúč biometrických údajov sa nezhoduje" + }, "biometricsNotEnabledTitle": { "message": "Biometria nie je povolená" }, @@ -2328,7 +2375,7 @@ "message": "Vaše hlavné heslo nespĺňa jednu alebo viacero podmienok vašej organizácie. Ak chcete získať prístup k trezoru, musíte teraz aktualizovať svoje hlavné heslo. Pokračovaním sa odhlásite z aktuálnej relácie a budete sa musieť znova prihlásiť. Aktívne relácie na iných zariadeniach môžu zostať aktívne až jednu hodinu." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Vaša organizácia zakázala šifrovanie dôveryhodného zariadenia. Na prístup k trezoru nastavte hlavné heslo." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrácia" @@ -3831,22 +3878,22 @@ "message": "Kľúč overovacej aplikácie" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Možnosti automatického vypĺňania" }, "websiteUri": { - "message": "Website (URI)" + "message": "Webová stránka (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Webová stránka pridaná" }, "addWebsite": { - "message": "Add website" + "message": "Pridať webovú stránku" }, "deleteWebsite": { - "message": "Delete website" + "message": "Odstrániť webovú stránku" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Predvolené ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Zobraziť spôsob mapovania $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Skryť spôsob mapovania $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Automaticky vyplniť pri načítaní stránky?" }, "cardDetails": { "message": "Podrobnosti o karte" @@ -3897,6 +3944,18 @@ "data": { "message": "Údaje" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Priradiť" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Umiestnenie položky" }, + "fileSends": { + "message": "Sendy so súborom" + }, + "textSends": { + "message": "Textové Sendy" + }, "bitwardenNewLook": { "message": "Bitwarden má nový vzhľad!" }, @@ -4098,6 +4163,12 @@ "message": "Automatické vypĺňanie a vyhľadávanie na karte Trezor je jednoduchšie a intuitívnejšie ako kedykoľvek predtým. Poobzerajte sa!" }, "accountActions": { - "message": "Account actions" + "message": "Operácie s účtom" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 0e2ae6a0bcd..540b0a3239a 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Uredi mapo" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Izbriši mapo" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Dejanje ob poteku roka" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 91029cf9c17..7e0f6fba670 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Уреди фасциклу" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Избриши фасциклу" }, @@ -1343,13 +1361,13 @@ "message": "Отвори сеф у бочну траку" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Аутоматско попуњавање последњу коришћену пријаву за тренутну веб страницу" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Аутоматско попуњавање последњу коришћену картицу за тренутну веб страницу" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Аутоматско попуњавање последњи коришћен идентитет за тренутну веб страницу" }, "commandGeneratePasswordDesc": { "message": "Генеришите и копирајте нову случајну лозинку у привремену меморију" @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Основни домен (препоручено)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Једна или више смерница организације утичу на поставке вашег генератора." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Акција на тајмаут сефа" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Неподударање налога" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Биометрија није омогућена" }, @@ -2328,7 +2375,7 @@ "message": "Ваша главна лозинка не испуњава једну или више смерница ваше организације. Да бисте приступили сефу, морате одмах да ажурирате главну лозинку. Ако наставите, одјавићете се са ваше тренутне сесије, што захтева да се поново пријавите. Активне сесије на другим уређајима могу да остану активне до један сат." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Ваша организација је онемогућила шифровање поузданог уређаја. Поставите главну лозинку за приступ вашем трезору." }, "resetPasswordPolicyAutoEnroll": { "message": "Ауто пријављивање" @@ -2787,16 +2834,16 @@ "message": "Промени пречицу" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Управљање пречицама" }, "autofillShortcut": { "message": "Пречице Ауто-пуњења" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "Пречица за ауто-попуњавање није подешена. Промените ово у подешавањима претраживача." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "Пречица за пријављивање за аутоматско попуњавање је $COMMAND$. Управљајте свим пречицама у подешавањима претраживача.", "placeholders": { "command": { "content": "$1", @@ -3831,22 +3878,22 @@ "message": "Кључ аутентификатора" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "Опције Ауто-пуњења" }, "websiteUri": { - "message": "Website (URI)" + "message": "Вебсајт (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "Вебсајт додат" }, "addWebsite": { - "message": "Add website" + "message": "Додај вебсајт" }, "deleteWebsite": { - "message": "Delete website" + "message": "Обриши вебсајт" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Подразумевано ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Прикажи откривање подударања $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Сакриј откривање подударања $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Ауто-попуњавање при учитавању странице?" }, "cardDetails": { "message": "Детаљи картице" @@ -3897,6 +3944,18 @@ "data": { "message": "Подаци" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Додели" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Смештај ставке" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden има нови изглед!" }, @@ -4098,6 +4163,12 @@ "message": "Лакше је и интуитивније него икада да се аутоматски попуњава и тражи са картице Сефа. Проверите!" }, "accountActions": { - "message": "Account actions" + "message": "Акције везане за налог" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 41fa8fc130c..5b62f6d45fe 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Redigera mapp" }, + "newFolder": { + "message": "Ny mapp" + }, + "folderName": { + "message": "Mappnamn" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Skapa mappar för att organisera dina valvobjekt" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Radera mapp" }, @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Basdomän (rekommenderas)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "En eller flera organisationspolicyer påverkar dina generatorinställningar." }, + "passwordGenerator": { + "message": "Lösenordsgenerator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Åtgärd när valvets tidsgräns överskrids" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Kontoavvikelse" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometri är inte aktiverat" }, @@ -2787,7 +2834,7 @@ "message": "Ändra genväg" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Hantera genvägar" }, "autofillShortcut": { "message": "Tangentbordsgenväg för automatisk ifyllnad" @@ -3834,19 +3881,19 @@ "message": "Auto-fill options" }, "websiteUri": { - "message": "Website (URI)" + "message": "Webbplats (URI)" }, "websiteAdded": { "message": "Website added" }, "addWebsite": { - "message": "Add website" + "message": "Lägg till webbplats" }, "deleteWebsite": { - "message": "Delete website" + "message": "Radera webbplats" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Lösenord", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Tilldela" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden har fått ett nytt utseende!" }, @@ -4098,6 +4163,12 @@ "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" }, "accountActions": { - "message": "Account actions" + "message": "Kontoåtgärder" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 5b39467dbe0..4b4cec42bba 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Edit folder" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Delete folder" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Vault timeout action" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index d4a834a253c..9bff3c0e79d 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "แก้ไขโฟลเดอร์" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "ลบโฟลเดอร์" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "นโยบายองค์กรอย่างน้อยหนึ่งนโยบายส่งผลต่อการตั้งค่าตัวสร้างของคุณ" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "การดำเนินการหลังหมดเวลาล็อคตู้เซฟ" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account missmatch" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Biometrics not set up" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 0bd43e67b5e..69acc9ba9b9 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Klasörü düzenle" }, + "newFolder": { + "message": "Yeni klasör" + }, + "folderName": { + "message": "Klasör adı" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "Hiç klasör eklenmedi" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Bu klasörü kalıcı olarak silmek istediğinizden emin misiniz?" + }, "deleteFolder": { "message": "Klasörü sil" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Bir ya da daha fazla kuruluş ilkesi, oluşturucu ayarlarınızı etkiliyor." }, + "passwordGenerator": { + "message": "Parola üreteci" + }, + "usernameGenerator": { + "message": "Kullanıcı adı üreteci" + }, + "useThisPassword": { + "message": "Bu parolayı kullan" + }, + "useThisUsername": { + "message": "Bu kullanıcı adını kullan" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Kasa zaman aşımı eylemi" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Hesap uyuşmazlığı" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biyometrik kilit açma başarısız oldu. Biyometrik gizli anahtarınız kasanın kilidini açamadı. Lütfen biyometriyi yeniden ayarlamayı deneyin." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biyometrik anahtar eşleşmedi" + }, "biometricsNotEnabledTitle": { "message": "Biyometri ayarlanmamış" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Veri" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Ata" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Kayıt konumu" }, + "fileSends": { + "message": "Dosya Send'leri" + }, + "textSends": { + "message": "Metin Send'leri" + }, "bitwardenNewLook": { "message": "Bitwarden'ın tasarımı güncellendi!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Otomatik öneri sayısını uzantı simgesinde göster" + }, + "systemDefault": { + "message": "Sistem varsayılanı" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b8ede486c6f..2f26091d8a6 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -141,7 +141,7 @@ "message": "Автозаповнення картки" }, "autoFillIdentity": { - "message": "Автозаповнення особистих даних" + "message": "Автозаповнення посвідчень" }, "generatePasswordCopied": { "message": "Генерувати пароль (з копіюванням)" @@ -165,7 +165,7 @@ "message": "Додати картку" }, "addIdentityMenu": { - "message": "Додати особисті дані" + "message": "Додати посвідчення" }, "unlockVaultMenu": { "message": "Розблокуйте сховище" @@ -304,6 +304,24 @@ "editFolder": { "message": "Редагування" }, + "newFolder": { + "message": "Нова тека" + }, + "folderName": { + "message": "Назва теки" + }, + "folderHintText": { + "message": "Зробіть теку вкладеною, вказавши після основної теки \"/\". Наприклад: Обговорення/Форуми" + }, + "noFoldersAdded": { + "message": "Немає доданих тек" + }, + "createFoldersToOrganize": { + "message": "Створіть теки для організації записів у сховищі" + }, + "deleteFolderPermanently": { + "message": "Ви дійсно хочете остаточно видалити цю теку?" + }, "deleteFolder": { "message": "Видалити теку" }, @@ -652,7 +670,7 @@ } }, "autofillError": { - "message": "Не вдається заповнити пароль на цій сторінці. Скопіюйте і вставте ім'я користувача та/або пароль." + "message": "Не вдається заповнити вибраний запис на цій сторінці. Скопіюйте і вставте інформацію вручну." }, "totpCaptureError": { "message": "Неможливо сканувати QR-код з поточної сторінки" @@ -712,7 +730,7 @@ "message": "Сталася неочікувана помилка." }, "nameRequired": { - "message": "Потрібна назва." + "message": "Необхідно ввести назву." }, "addedFolder": { "message": "Теку додано" @@ -1349,7 +1367,7 @@ "message": "Автозаповнення останньої використаної картки для цього вебсайту" }, "commandAutofillIdentityDesc": { - "message": "Автозаповнення останніх використаних особистих даних для цього вебсайту" + "message": "Автозаповнення останнього використаного посвідчення для цього вебсайту" }, "commandGeneratePasswordDesc": { "message": "Генерувати і копіювати новий випадковий пароль в буфер обміну" @@ -1504,7 +1522,7 @@ "message": "Повне ім'я" }, "identityName": { - "message": "Назва" + "message": "Назва посвідчення" }, "company": { "message": "Компанія" @@ -1564,7 +1582,7 @@ "message": "Картка" }, "typeIdentity": { - "message": "Особисті дані" + "message": "Посвідчення" }, "newItemHeader": { "message": "Новий $TYPE$", @@ -1704,7 +1722,7 @@ "message": "Типи" }, "allItems": { - "message": "Всі елементи" + "message": "Усі записи" }, "noPasswordsInList": { "message": "Немає паролів." @@ -1731,7 +1749,7 @@ "message": "Ви впевнені, що ніколи не хочете блокувати? Встановивши цю опцію, ключ шифрування вашого сховища зберігатиметься на вашому пристрої. Використовуючи цю опцію, вам слід бути певними в тому, що ваш пристрій має належний захист." }, "noOrganizationsList": { - "message": "Ви не входите до жодної організації. Організації дозволяють безпечно обмінюватися елементами з іншими користувачами." + "message": "Ви не входите до жодної організації. Організації дають змогу безпечно обмінюватися записами з іншими користувачами." }, "noCollectionsInList": { "message": "Немає збірок." @@ -1740,7 +1758,7 @@ "message": "Власник" }, "whoOwnsThisItem": { - "message": "Хто є власником цього елемента?" + "message": "Хто є власником цього запису?" }, "strong": { "message": "Надійний", @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "На параметри генератора впливають одна чи декілька політик організації." }, + "passwordGenerator": { + "message": "Генератор паролів" + }, + "usernameGenerator": { + "message": "Генератор імені користувача" + }, + "useThisPassword": { + "message": "Використати цей пароль" + }, + "useThisUsername": { + "message": "Використати це ім'я користувача" + }, + "securePasswordGenerated": { + "message": "Надійний пароль згенеровано! Обов'язково оновіть свій пароль на вебсайті." + }, + "useGeneratorHelpTextPartOne": { + "message": "Скористатися генератором", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "для створення надійного, унікального пароля", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Дія після часу очікування сховища" }, @@ -1851,7 +1892,7 @@ "message": "Запис заповнено і збережено" }, "autoFillSuccess": { - "message": "Запис заповнено" + "message": "Запис заповнено " }, "insecurePageWarning": { "message": "Попередження: це незахищена сторінка HTTP, тому будь-яка інформація, яку ви передаєте, потенційно може бути переглянута чи змінена сторонніми. Ці облікові дані було збережено на безпечній сторінці (HTTPS)." @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Невідповідність облікових записів" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Збій біометричного розблокування. Біометричний секретний ключ не зміг розблокувати сховище. Спробуйте налаштувати біометрію знову." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Біометричний ключ відрізняється" + }, "biometricsNotEnabledTitle": { "message": "Біометрію не налаштовано" }, @@ -2043,7 +2090,7 @@ "message": "Політика організації впливає на ваші параметри власності." }, "personalOwnershipPolicyInEffectImports": { - "message": "Політика організації заблокувала імпортування елементів до вашого особистого сховища." + "message": "Політика організації заблокувала імпортування записів до вашого особистого сховища." }, "domainsTitle": { "message": "Домени", @@ -2328,7 +2375,7 @@ "message": "Ваш головний пароль не відповідає одній або більше політикам вашої організації. Щоб отримати доступ до сховища, вам необхідно оновити свій головний пароль зараз. Продовживши, ви вийдете з поточного сеансу, після чого потрібно буде повторно виконати вхід. Сеанси на інших пристроях можуть залишатися активними протягом однієї години." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "Ваша організація вимкнула шифрування довірених пристроїв. Встановіть головний пароль для доступу до сховища." }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматичне розгортання" @@ -2461,7 +2508,7 @@ "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Записи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", @@ -2763,7 +2810,7 @@ "message": "Як працює автозаповнення" }, "autofillSelectInfoWithCommand": { - "message": "Виберіть елемент із цього екрану, скористайтеся комбінацією клавіш $COMMAND$, або дізнайтеся про інші можливості в налаштуваннях.", + "message": "Виберіть об'єкт із цього екрану, скористайтеся комбінацією клавіш $COMMAND$, або дізнайтеся про інші можливості в налаштуваннях.", "placeholders": { "command": { "content": "$1", @@ -2772,7 +2819,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Виберіть елемент із цього екрану або дізнайтеся про інші можливості в налаштуваннях." + "message": "Виберіть об'єкт із цього екрану або дізнайтеся про інші можливості в налаштуваннях." }, "gotIt": { "message": "Зрозуміло" @@ -3140,11 +3187,11 @@ "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "Особисті дані", + "message": "Нове посвідчення", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Додавання нового запису для особистих даних – відкриється нове вікно", + "message": "Додавання нового запису для посвідчення – відкриється нове вікно", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3177,7 +3224,7 @@ "message": "Дані успішно імпортовано" }, "importSuccessNumberOfItems": { - "message": "Всього імпортовано $AMOUNT$ елементів.", + "message": "Всього імпортовано $AMOUNT$ записів.", "placeholders": { "amount": { "content": "$1", @@ -3286,7 +3333,7 @@ } }, "importUnassignedItemsError": { - "message": "Файл містить непризначені елементи." + "message": "Файл містить непризначені записи." }, "selectFormat": { "message": "Оберіть формат імпортованого файлу" @@ -3368,7 +3415,7 @@ "message": "Перезаписати ключ доступу?" }, "overwritePasskeyAlert": { - "message": "Цей елемент вже містить ключ доступу. Ви впевнені, що хочете перезаписати поточний ключ доступу?" + "message": "Цей запис уже містить ключ доступу. Ви впевнені, що хочете перезаписати поточний ключ доступу?" }, "featureNotSupported": { "message": "Функція ще не підтримується" @@ -3715,7 +3762,7 @@ } }, "itemsWithNoFolder": { - "message": "Елементи без теки" + "message": "Записи без теки" }, "itemDetails": { "message": "Подробиці запису" @@ -3743,7 +3790,7 @@ "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Елементи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." + "message": "Записи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." }, "additionalInformation": { "message": "Додаткова інформація" @@ -3897,6 +3944,18 @@ "data": { "message": "Дані" }, + "passkeys": { + "message": "Ключі доступу", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Паролі", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Увійти з ключем доступу", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Призначити" }, @@ -4005,10 +4064,10 @@ "message": "Оберіть збірки для призначення" }, "personalItemTransferWarningSingular": { - "message": "1 елемент буде остаточно перенесено до вибраної організації. Ви більше не будете власником цього елемента." + "message": "1 запис буде остаточно перенесено до вибраної організації. Ви більше не будете власником цього запису." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ елементи буде остаточно перенесено до вибраної організації. Ви більше не будете власником цих елементів.", + "message": "$PERSONAL_ITEMS_COUNT$ записів будуть остаточно перенесені до вибраної організації. Ви більше не будете власником цих записів.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4017,7 +4076,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 елемент буде остаточно перенесено до $ORG$. Ви більше не будете власником цього елемента.", + "message": "1 запис буде остаточно перенесено до $ORG$. Ви більше не будете власником цього запису.", "placeholders": { "org": { "content": "$1", @@ -4026,7 +4085,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ елементи буде остаточно перенесено до $ORG$. Ви більше не будете власником цих елементів.", + "message": "$PERSONAL_ITEMS_COUNT$ записів будуть остаточно перенесені до $ORG$. Ви більше не будете власником цих записів.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4054,7 +4113,7 @@ } }, "itemsMovedToOrg": { - "message": "Елементи переміщено до $ORGNAME$", + "message": "Записи переміщено до $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4063,7 +4122,7 @@ } }, "itemMovedToOrg": { - "message": "Елемент переміщено до $ORGNAME$", + "message": "Запис переміщено до $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4089,7 +4148,13 @@ } }, "itemLocation": { - "message": "Розташування елемента" + "message": "Розташування запису" + }, + "fileSends": { + "message": "Відправлення файлів" + }, + "textSends": { + "message": "Відправлення тексту" }, "bitwardenNewLook": { "message": "Bitwarden має новий вигляд!" @@ -4098,6 +4163,12 @@ "message": "Ще простіше автозаповнення та інтуїтивніший пошук у сховищі. Ознайомтеся!" }, "accountActions": { - "message": "Account actions" + "message": "Дії з обліковим записом" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index bd33243824f..38230fa91b0 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "Chỉnh sửa thư mục" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "Xóa thư mục" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Các chính sách của tổ chức đang ảnh hưởng đến cài đặt tạo mật khẩu của bạn." }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "Hành động khi hết thời gian chờ của kho lưu trữ" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "Tài khoản không đúng" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "Sinh trắc học chưa được cài đặt" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Dữ liệu" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Gán" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Vị trí mục" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fc4da25af27..1e4a9c964c2 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -120,16 +120,16 @@ "message": "复制名称" }, "copyCompany": { - "message": "复制公司" + "message": "复制公司信息" }, "copySSN": { - "message": "复制社会安全号码" + "message": "复制社会保障号码" }, "copyPassportNumber": { "message": "复制护照号码" }, "copyLicenseNumber": { - "message": "复制驾照号码" + "message": "复制许可证号码" }, "autoFill": { "message": "自动填充" @@ -304,6 +304,24 @@ "editFolder": { "message": "编辑文件夹" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "删除文件夹" }, @@ -345,7 +363,7 @@ "message": "自动生成安全可靠唯一的登录密码。" }, "bitWebVaultApp": { - "message": "Bitwarden 网页版应用" + "message": "Bitwarden 网页 App" }, "importItems": { "message": "导入项目" @@ -652,7 +670,7 @@ } }, "autofillError": { - "message": "无法在此页面上自动填充所选项目。请改为手工复制并粘贴。" + "message": "无法在此页面上自动填充所选项目。请改为手动复制并粘贴。" }, "totpCaptureError": { "message": "无法从当前网页扫描二维码" @@ -1104,7 +1122,7 @@ "message": "自动复制 TOTP" }, "disableAutoTotpCopyDesc": { - "message": "如果登录包含验证器密钥,当自动填充此登录时,TOTP 验证码将复制到剪贴板。" + "message": "如果登录包含验证器密钥,当自动填充此登录时,将 TOTP 验证码复制到剪贴板。" }, "enableAutoBiometricsPrompt": { "message": "启动时要求生物识别" @@ -1343,13 +1361,13 @@ "message": "在侧边栏中打开密码库" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "为当前网站自动填充最后一次使用的登录信息" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "为当前网站自动填充最后一次使用的支付卡信息" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "为当前网站自动填充最后一次使用的身份信息" }, "commandGeneratePasswordDesc": { "message": "生成一个新的随机密码并将其复制到剪贴板中。" @@ -1510,7 +1528,7 @@ "message": "公司" }, "ssn": { - "message": "社会保险号码" + "message": "社会保障号码" }, "passportNumber": { "message": "护照号码" @@ -1656,7 +1674,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "基础域(推荐)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "一个或多个组织策略正在影响您的生成器设置。" }, + "passwordGenerator": { + "message": "密码生成器" + }, + "usernameGenerator": { + "message": "用户名生成器" + }, + "useThisPassword": { + "message": "使用此密码" + }, + "useThisUsername": { + "message": "使用此用户名" + }, + "securePasswordGenerated": { + "message": "安全密码生成好了!别忘了也在网站上更新一下您的密码。" + }, + "useGeneratorHelpTextPartOne": { + "message": "使用此生成器", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "创建一个强大且唯一的密码", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "密码库超时动作" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "账户不匹配" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "生物识别解锁失败。生物识别安全钥匙解锁密码库失败。请尝试重新设置生物识别。" + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "生物识别密钥不匹配" + }, "biometricsNotEnabledTitle": { "message": "生物识别未设置" }, @@ -2328,7 +2375,7 @@ "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "您的组织禁用了信任设备加密。要访问您的密码库,请设置一个主密码。" }, "resetPasswordPolicyAutoEnroll": { "message": "自动注册" @@ -2757,7 +2804,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "您的组织策略已开启在页面加载时的自动填充。" + "message": "您的组织策略已开启页面加载时自动填充。" }, "howToAutofill": { "message": "如何自动填充" @@ -2787,16 +2834,16 @@ "message": "更改快捷键" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "管理快捷方式" + "message": "管理快捷键" }, "autofillShortcut": { "message": "自动填充键盘快捷键" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "未设置自动填充登录快捷键。请在浏览器设置中更改它。" }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "自动填充登录快捷键是 $COMMAND$。请在浏览器设置中管理所有快捷键。", "placeholders": { "command": { "content": "$1", @@ -2805,7 +2852,7 @@ } }, "autofillShortcutTextSafari": { - "message": "默认自动填充快捷方式:$COMMAND$。", + "message": "默认的自动填充快捷键:$COMMAND$", "placeholders": { "command": { "content": "$1", @@ -3062,7 +3109,7 @@ "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "页面加载时自动填充设置为默认设置。", + "message": "页面加载时自动填充设置为使用默认设置。", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { @@ -3831,22 +3878,22 @@ "message": "验证器密钥" }, "autofillOptions": { - "message": "Auto-fill options" + "message": "自动填充选项" }, "websiteUri": { - "message": "Website (URI)" + "message": "网站 (URI)" }, "websiteAdded": { - "message": "Website added" + "message": "网址已添加" }, "addWebsite": { - "message": "Add website" + "message": "添加网站" }, "deleteWebsite": { - "message": "Delete website" + "message": "删除网站" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "默认 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -3856,7 +3903,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "显示匹配检测 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3865,7 +3912,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "隐藏匹配检测 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -3874,7 +3921,7 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "页面加载时自动填充吗?" }, "cardDetails": { "message": "支付卡详情" @@ -3897,6 +3944,18 @@ "data": { "message": "数据" }, + "passkeys": { + "message": "通行密钥", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "密码", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "使用通行密钥登录", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "分配" }, @@ -4091,13 +4150,25 @@ "itemLocation": { "message": "项目位置" }, + "fileSends": { + "message": "文件 Send" + }, + "textSends": { + "message": "文本 Send" + }, "bitwardenNewLook": { - "message": "Bitwarden 具有一个新的外观!" + "message": "Bitwarden 拥有一个新的外观!" }, "bitwardenNewLookDesc": { "message": "从密码库标签页自动填充和搜索比以往任何时候都更简单直观。来看看吧!" }, "accountActions": { - "message": "Account actions" + "message": "账户操作" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1d380358613..7e1a4bff408 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -304,6 +304,24 @@ "editFolder": { "message": "編輯資料夾" }, + "newFolder": { + "message": "New folder" + }, + "folderName": { + "message": "Folder name" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "No folders added" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "Are you sure you want to permanently delete this folder?" + }, "deleteFolder": { "message": "刪除資料夾" }, @@ -1803,6 +1821,29 @@ "passwordGeneratorPolicyInEffect": { "message": "一個或多個組織原則正影響密碼產生器設定。" }, + "passwordGenerator": { + "message": "Password generator" + }, + "usernameGenerator": { + "message": "Username generator" + }, + "useThisPassword": { + "message": "Use this password" + }, + "useThisUsername": { + "message": "Use this username" + }, + "securePasswordGenerated": { + "message": "Secure password generated! Don't forget to also update your password on the website." + }, + "useGeneratorHelpTextPartOne": { + "message": "Use the generator", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, + "useGeneratorHelpTextPartTwo": { + "message": "to create a strong unique password", + "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" + }, "vaultTimeoutAction": { "message": "密碼庫逾時動作" }, @@ -2000,6 +2041,12 @@ "nativeMessagingWrongUserTitle": { "message": "帳戶不相符" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometric key missmatch" + }, "biometricsNotEnabledTitle": { "message": "生物特徵辨識未設定" }, @@ -3897,6 +3944,18 @@ "data": { "message": "Data" }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Passwords", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Log in with passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, "assign": { "message": "Assign" }, @@ -4091,6 +4150,12 @@ "itemLocation": { "message": "Item Location" }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, "bitwardenNewLook": { "message": "Bitwarden has a new look!" }, @@ -4099,5 +4164,11 @@ }, "accountActions": { "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" } } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index e7082f40196..d5273fd9fb2 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -20,6 +20,7 @@ [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="true" [decreaseTopPadding]="true" + [maxWidth]="maxWidth" > diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index df6e313342b..7a5b156a506 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -149,7 +149,15 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { } if (data.pageSubtitle) { - this.pageSubtitle = this.i18nService.t(data.pageSubtitle); + // If you pass just a string, we translate it by default + if (typeof data.pageSubtitle === "string") { + this.pageSubtitle = this.i18nService.t(data.pageSubtitle); + } else { + // if you pass an object, you can specify if you want to translate it or not + this.pageSubtitle = data.pageSubtitle.translate + ? this.i18nService.t(data.pageSubtitle.subtitle) + : data.pageSubtitle.subtitle; + } } if (data.pageIcon) { @@ -181,6 +189,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showAcctSwitcher = null; this.showBackButton = null; this.showLogo = null; + this.maxWidth = null; } ngOnDestroy() { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index c447ccffd78..44060f991ff 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -28,6 +28,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; +import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service"; import { @@ -145,7 +146,15 @@ const decorators = (options: { ], }), applicationConfig({ - providers: [importProvidersFrom(RouterModule.forRoot(options.routes))], + providers: [ + importProvidersFrom(RouterModule.forRoot(options.routes)), + { + provide: PopupRouterCacheService, + useValue: { + back() {}, + } as Partial, + }, + ], }), ]; }; diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 820cdf11cdc..076c03801aa 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { @@ -382,49 +383,73 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { return; } - const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService); - const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed); + let awaitDesktopDialogRef: DialogRef | undefined; + let biometricsResponseReceived = false; await this.cryptoService.refreshAdditionalKeys(); - await Promise.race([ - awaitDesktopDialogClosed.then(async (result) => { - if (result !== true) { - this.form.controls.biometric.setValue(false); - } - }), - this.platformUtilsService - .authenticateBiometric() - .then((result) => { - this.form.controls.biometric.setValue(result); - if (!result) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorEnableBiometricTitle"), - this.i18nService.t("errorEnableBiometricDesc"), - ); - } - }) - .catch((e) => { - // Handle connection errors - this.form.controls.biometric.setValue(false); + const waitForUserDialogPromise = async () => { + // only show waiting dialog if we have waited for 200 msec to prevent double dialog + // the os will respond instantly if the dialog shows successfully, and the desktop app will respond instantly if something is wrong + await new Promise((resolve) => setTimeout(resolve, 200)); + if (biometricsResponseReceived) { + return; + } - const error = BiometricErrors[e.message as BiometricErrorTypes]; + awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService); + const result = await firstValueFrom(awaitDesktopDialogRef.closed); + if (result !== true) { + this.form.controls.biometric.setValue(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.dialogService.openSimpleDialog({ - title: { key: error.title }, - content: { key: error.description }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - }) - .finally(() => { + const biometricsPromise = async () => { + try { + const result = await this.platformUtilsService.authenticateBiometric(); + + // prevent duplicate dialog + biometricsResponseReceived = true; + if (awaitDesktopDialogRef) { awaitDesktopDialogRef.close(true); - }), - ]); + } + + this.form.controls.biometric.setValue(result); + if (!result) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorEnableBiometricTitle"), + this.i18nService.t("errorEnableBiometricDesc"), + ); + } + } catch (e) { + // prevent duplicate dialog + biometricsResponseReceived = true; + if (awaitDesktopDialogRef) { + awaitDesktopDialogRef.close(true); + } + + this.form.controls.biometric.setValue(false); + + if (e.message == "canceled") { + return; + } + + const error = BiometricErrors[e.message as BiometricErrorTypes]; + await this.dialogService.openSimpleDialog({ + title: { key: error.title }, + content: { key: error.description }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + } finally { + if (awaitDesktopDialogRef) { + awaitDesktopDialogRef.close(true); + } + } + }; + + await Promise.race([waitForUserDialogPromise(), biometricsPromise()]); } else { await this.biometricStateService.setBiometricUnlockEnabled(false); await this.biometricStateService.setFingerprintValidated(false); diff --git a/apps/browser/src/autofill/background/abstractions/auto-submit-login.background.ts b/apps/browser/src/autofill/background/abstractions/auto-submit-login.background.ts new file mode 100644 index 00000000000..eb5b2cfd761 --- /dev/null +++ b/apps/browser/src/autofill/background/abstractions/auto-submit-login.background.ts @@ -0,0 +1,21 @@ +import AutofillPageDetails from "../../models/autofill-page-details"; + +export type AutoSubmitLoginMessage = { + command: string; + pageDetails?: AutofillPageDetails; +}; + +export type AutoSubmitLoginMessageParams = { + message: AutoSubmitLoginMessage; + sender: chrome.runtime.MessageSender; +}; + +export type AutoSubmitLoginBackgroundExtensionMessageHandlers = { + [key: string]: ({ message, sender }: AutoSubmitLoginMessageParams) => any; + triggerAutoSubmitLogin: ({ message, sender }: AutoSubmitLoginMessageParams) => Promise; + multiStepAutoSubmitLoginComplete: ({ sender }: AutoSubmitLoginMessageParams) => void; +}; + +export abstract class AutoSubmitLoginBackground { + abstract init(): void; +} diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 8122f5c4ed9..950f3b8e275 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -1,5 +1,6 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import AutofillPageDetails from "../../models/autofill-page-details"; import { PageDetail } from "../../services/abstractions/autofill.service"; @@ -132,6 +133,7 @@ export type OverlayPortMessage = { direction?: string; inlineMenuCipherId?: string; addNewCipherType?: CipherType; + usePasskey?: boolean; }; export type InlineMenuCipherData = { @@ -142,7 +144,13 @@ export type InlineMenuCipherData = { favorite: boolean; icon: WebsiteIconData; accountCreationFieldType?: string; - login?: { username: string }; + login?: { + username: string; + passkey: { + rpName: string; + userName: string; + } | null; + }; card?: string; identity?: { fullName: string; @@ -150,6 +158,15 @@ export type InlineMenuCipherData = { }; }; +export type BuildCipherDataParams = { + inlineMenuCipherId: string; + cipher: CipherView; + showFavicons?: boolean; + showInlineMenuAccountCreation?: boolean; + hasPasskey?: boolean; + identityData?: { fullName: string; username?: string }; +}; + export type BackgroundMessageParam = { message: OverlayBackgroundExtensionMessage; }; diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts new file mode 100644 index 00000000000..ea86a84d63f --- /dev/null +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -0,0 +1,503 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +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 { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import AutofillPageDetails from "../models/autofill-page-details"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { + flushPromises, + sendMockExtensionMessage, + triggerTabOnActivatedEvent, + triggerTabOnRemovedEvent, + triggerTabOnUpdatedEvent, + triggerWebNavigationOnCompletedEvent, + triggerWebRequestOnBeforeRedirectEvent, + triggerWebRequestOnBeforeRequestEvent, +} from "../spec/testing-utils"; + +import { AutoSubmitLoginBackground } from "./auto-submit-login.background"; + +describe("AutoSubmitLoginBackground", () => { + let logService: MockProxy; + let autofillService: MockProxy; + let scriptInjectorService: MockProxy; + let authStatus$: BehaviorSubject; + let authService: MockProxy; + let configService: MockProxy; + let platformUtilsService: MockProxy; + let policyDetails: MockProxy; + let automaticAppLogInPolicy$: BehaviorSubject; + let policyAppliesToActiveUser$: BehaviorSubject; + let policyService: MockProxy; + let autoSubmitLoginBackground: AutoSubmitLoginBackground; + const validIpdUrl1 = "https://example.com"; + const validIpdUrl2 = "https://subdomain.example3.com"; + const validAutoSubmitHost = "some-valid-url.com"; + const validAutoSubmitUrl = `https://${validAutoSubmitHost}/?autofill=1`; + + beforeEach(() => { + logService = mock(); + autofillService = mock(); + scriptInjectorService = mock(); + authStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + authService = mock(); + authService.activeAccountStatus$ = authStatus$; + configService = mock({ + getFeatureFlag: jest.fn().mockResolvedValue(true), + }); + platformUtilsService = mock(); + policyDetails = mock({ + enabled: true, + data: { + idpHost: `${validIpdUrl1} , https://example2.com/some/sub-route ,${validIpdUrl2}, [invalidValue] ,,`, + }, + }); + automaticAppLogInPolicy$ = new BehaviorSubject(policyDetails); + policyAppliesToActiveUser$ = new BehaviorSubject(true); + policyService = mock({ + get$: jest.fn().mockReturnValue(automaticAppLogInPolicy$), + policyAppliesToActiveUser$: jest.fn().mockReturnValue(policyAppliesToActiveUser$), + }); + autoSubmitLoginBackground = new AutoSubmitLoginBackground( + logService, + autofillService, + scriptInjectorService, + authService, + configService, + platformUtilsService, + policyService, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("when the AutoSubmitLoginBackground feature is disabled", () => { + it("destroys all event listeners when the AutomaticAppLogIn policy is not enabled", async () => { + automaticAppLogInPolicy$.next(mock({ ...policyDetails, enabled: false })); + + await autoSubmitLoginBackground.init(); + + expect(chrome.webRequest.onBeforeRequest.removeListener).toHaveBeenCalled(); + }); + + it("destroys all event listeners when the AutomaticAppLogIn policy does not apply to the current user", async () => { + policyAppliesToActiveUser$.next(false); + + await autoSubmitLoginBackground.init(); + + expect(chrome.webRequest.onBeforeRequest.removeListener).toHaveBeenCalled(); + }); + + it("destroys all event listeners when the idpHost is not specified in the AutomaticAppLogIn policy", async () => { + automaticAppLogInPolicy$.next(mock({ ...policyDetails, data: { idpHost: "" } })); + + await autoSubmitLoginBackground.init(); + + expect(chrome.webRequest.onBeforeRequest.addListener).not.toHaveBeenCalled(); + }); + }); + + describe("when the AutoSubmitLoginBackground feature is enabled", () => { + let webRequestDetails: chrome.webRequest.WebRequestBodyDetails; + + describe("starting the auto-submit login workflow", () => { + beforeEach(async () => { + webRequestDetails = mock({ + initiator: validIpdUrl1, + url: validAutoSubmitUrl, + type: "main_frame", + tabId: 1, + }); + await autoSubmitLoginBackground.init(); + }); + + it("sets up the auto-submit workflow when the web request occurs in the main frame and the destination URL contains a valid auto-fill param", () => { + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(autoSubmitLoginBackground["currentAutoSubmitHostData"]).toStrictEqual({ + url: validAutoSubmitUrl, + tabId: webRequestDetails.tabId, + }); + expect(chrome.webNavigation.onCompleted.addListener).toBeCalledWith(expect.any(Function), { + url: [{ hostEquals: validAutoSubmitHost }], + }); + }); + + it("sets up the auto-submit workflow when the web request occurs in a sub frame and the initiator of the request is a valid auto-submit host", async () => { + const topFrameHost = "some-top-frame.com"; + const subFrameHost = "some-sub-frame.com"; + autoSubmitLoginBackground["validAutoSubmitHosts"].add(topFrameHost); + webRequestDetails.type = "sub_frame"; + webRequestDetails.initiator = `https://${topFrameHost}`; + webRequestDetails.url = `https://${subFrameHost}`; + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(chrome.webNavigation.onCompleted.addListener).toBeCalledWith(expect.any(Function), { + url: [{ hostEquals: subFrameHost }], + }); + }); + + describe("injecting the auto-submit login content script", () => { + let webNavigationDetails: chrome.webNavigation.WebNavigationFramedCallbackDetails; + + beforeEach(() => { + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + webNavigationDetails = mock({ + tabId: webRequestDetails.tabId, + url: webRequestDetails.url, + }); + }); + + it("skips injecting the content script when the routed-to url is invalid", () => { + webNavigationDetails.url = "[invalid-host]"; + + triggerWebNavigationOnCompletedEvent(webNavigationDetails); + + expect(scriptInjectorService.inject).not.toHaveBeenCalled(); + }); + + it("skips injecting the content script when the extension is not unlocked", async () => { + authStatus$.next(AuthenticationStatus.Locked); + + triggerWebNavigationOnCompletedEvent(webNavigationDetails); + await flushPromises(); + + expect(scriptInjectorService.inject).not.toHaveBeenCalled(); + }); + + it("injects the auto-submit login content script", async () => { + triggerWebNavigationOnCompletedEvent(webNavigationDetails); + await flushPromises(); + + expect(scriptInjectorService.inject).toBeCalledWith({ + tabId: webRequestDetails.tabId, + injectDetails: { + file: "content/auto-submit-login.js", + runAt: "document_start", + frame: "all_frames", + }, + }); + }); + }); + }); + + describe("cancelling an active auto-submit login workflow", () => { + beforeEach(async () => { + webRequestDetails = mock({ + initiator: validIpdUrl1, + url: validAutoSubmitUrl, + type: "main_frame", + }); + await autoSubmitLoginBackground.init(); + autoSubmitLoginBackground["currentAutoSubmitHostData"] = { + url: validAutoSubmitUrl, + tabId: webRequestDetails.tabId, + }; + autoSubmitLoginBackground["validAutoSubmitHosts"].add(validAutoSubmitHost); + }); + + it("clears the auto-submit data when a POST request is encountered during an active auto-submit login workflow", async () => { + webRequestDetails.method = "POST"; + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(autoSubmitLoginBackground["currentAutoSubmitHostData"]).toStrictEqual({}); + }); + + it("clears the auto-submit data when a redirection to an invalid host is made during an active auto-submit workflow", () => { + webRequestDetails.url = "https://invalid-host.com"; + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(autoSubmitLoginBackground["currentAutoSubmitHostData"]).toStrictEqual({}); + }); + + it("disables the auto-submit workflow if a web request is initiated after the auto-submit route has been visited", () => { + webRequestDetails.url = `https://${validAutoSubmitHost}`; + webRequestDetails.initiator = `https://${validAutoSubmitHost}?autofill=1`; + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(autoSubmitLoginBackground["validAutoSubmitHosts"].has(validAutoSubmitHost)).toBe( + false, + ); + }); + + it("disables the auto-submit workflow if a web request to a different page is initiated after the auto-submit route has been visited", async () => { + webRequestDetails.url = `https://${validAutoSubmitHost}/some-other-route.com`; + jest + .spyOn(BrowserApi, "getTab") + .mockResolvedValue(mock({ url: validAutoSubmitHost })); + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + await flushPromises(); + + expect(autoSubmitLoginBackground["validAutoSubmitHosts"].has(validAutoSubmitHost)).toBe( + false, + ); + }); + }); + + describe("when the extension is running on a Safari browser", () => { + const tabId = 1; + const tab = mock({ id: tabId, url: validIpdUrl1 }); + + beforeEach(() => { + platformUtilsService.isSafari.mockReturnValue(true); + autoSubmitLoginBackground = new AutoSubmitLoginBackground( + logService, + autofillService, + scriptInjectorService, + authService, + configService, + platformUtilsService, + policyService, + ); + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(tab); + }); + + it("sets the most recent IDP host to the current tab", async () => { + await autoSubmitLoginBackground.init(); + await flushPromises(); + + expect(autoSubmitLoginBackground["mostRecentIdpHost"]).toStrictEqual({ + url: validIpdUrl1, + tabId: tabId, + }); + }); + + describe("requests that occur within a sub-frame", () => { + const webRequestDetails = mock({ + url: validAutoSubmitUrl, + frameId: 1, + }); + + it("sets the initiator of the request to an empty value when the most recent IDP host has not be set", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); + await autoSubmitLoginBackground.init(); + await flushPromises(); + autoSubmitLoginBackground["validAutoSubmitHosts"].add(validAutoSubmitHost); + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(chrome.webNavigation.onCompleted.addListener).not.toHaveBeenCalledWith( + autoSubmitLoginBackground["handleAutoSubmitHostNavigationCompleted"], + { url: [{ hostEquals: validAutoSubmitHost }] }, + ); + }); + + it("treats the routed to url as the initiator of a request", async () => { + await autoSubmitLoginBackground.init(); + await flushPromises(); + autoSubmitLoginBackground["validAutoSubmitHosts"].add(validAutoSubmitHost); + + triggerWebRequestOnBeforeRequestEvent(webRequestDetails); + + expect(chrome.webNavigation.onCompleted.addListener).toBeCalledWith( + autoSubmitLoginBackground["handleAutoSubmitHostNavigationCompleted"], + { url: [{ hostEquals: validAutoSubmitHost }] }, + ); + }); + }); + + describe("event listeners that update the most recently visited IDP host", () => { + const newTabId = 2; + const newTab = mock({ id: newTabId, url: validIpdUrl2 }); + + beforeEach(async () => { + await autoSubmitLoginBackground.init(); + }); + + it("updates the most recent idp host when a tab is activated", async () => { + jest.spyOn(BrowserApi, "getTab").mockResolvedValue(newTab); + + triggerTabOnActivatedEvent(mock({ tabId: newTabId })); + await flushPromises(); + + expect(autoSubmitLoginBackground["mostRecentIdpHost"]).toStrictEqual({ + url: validIpdUrl2, + tabId: newTabId, + }); + }); + + it("updates the most recent id host when a tab is updated", () => { + triggerTabOnUpdatedEvent( + newTabId, + mock({ url: validIpdUrl1 }), + newTab, + ); + + expect(autoSubmitLoginBackground["mostRecentIdpHost"]).toStrictEqual({ + url: validIpdUrl1, + tabId: newTabId, + }); + }); + + describe("when a tab completes a navigation event", () => { + it("clears the set of valid auto-submit hosts", () => { + autoSubmitLoginBackground["validAutoSubmitHosts"].add(validIpdUrl1); + + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: newTabId, + url: validIpdUrl2, + frameId: 0, + }), + ); + + expect(autoSubmitLoginBackground["validAutoSubmitHosts"].size).toBe(0); + }); + + it("updates the most recent idp host", () => { + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: newTabId, + url: validIpdUrl2, + frameId: 0, + }), + ); + + expect(autoSubmitLoginBackground["mostRecentIdpHost"]).toStrictEqual({ + url: validIpdUrl2, + tabId: newTabId, + }); + }); + + it("clears the auto submit host data if the tab is removed or closed", () => { + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: newTabId, + url: validIpdUrl2, + frameId: 0, + }), + ); + autoSubmitLoginBackground["currentAutoSubmitHostData"] = { + url: validIpdUrl2, + tabId: newTabId, + }; + + triggerTabOnRemovedEvent(newTabId, mock()); + + expect(autoSubmitLoginBackground["currentAutoSubmitHostData"]).toStrictEqual({}); + }); + }); + }); + + it("allows the route to trigger auto-submit after a chain redirection to a valid auto-submit URL is made", async () => { + await autoSubmitLoginBackground.init(); + autoSubmitLoginBackground["mostRecentIdpHost"] = { + url: validIpdUrl1, + tabId: tabId, + }; + triggerWebRequestOnBeforeRedirectEvent( + mock({ + url: validIpdUrl1, + redirectUrl: validIpdUrl2, + frameId: 0, + }), + ); + triggerWebRequestOnBeforeRedirectEvent( + mock({ + url: validIpdUrl2, + redirectUrl: validAutoSubmitUrl, + frameId: 0, + }), + ); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + tabId: tabId, + url: `https://${validAutoSubmitHost}`, + initiator: null, + frameId: 0, + }), + ); + + expect(chrome.webNavigation.onCompleted.addListener).toBeCalledWith(expect.any(Function), { + url: [{ hostEquals: validAutoSubmitHost }], + }); + }); + }); + + describe("extension message listeners", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(async () => { + await autoSubmitLoginBackground.init(); + autoSubmitLoginBackground["validAutoSubmitHosts"].add(validAutoSubmitHost); + autoSubmitLoginBackground["currentAutoSubmitHostData"] = { + url: validAutoSubmitUrl, + tabId: 1, + }; + sender = mock({ + tab: mock({ id: 1 }), + frameId: 0, + url: validAutoSubmitUrl, + }); + }); + + it("skips acting on messages that do not come from the current auto-fill workflow's tab", () => { + sender.tab = mock({ id: 2 }); + + sendMockExtensionMessage({ command: "triggerAutoSubmitLogin" }, sender); + + expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; + }); + + it("skips acting on messages whose command does not have a registered handler", () => { + sendMockExtensionMessage({ command: "someInvalidCommand" }, sender); + + expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; + }); + + describe("triggerAutoSubmitLogin extension message", () => { + it("triggers an autofill action with auto-submission on the sender of the message", async () => { + const message = { + command: "triggerAutoSubmitLogin", + pageDetails: mock(), + }; + + sendMockExtensionMessage(message, sender); + await flushPromises(); + + expect(autofillService.doAutoFillOnTab).toBeCalledWith( + [ + { + frameId: sender.frameId, + tab: sender.tab, + details: message.pageDetails, + }, + ], + sender.tab, + true, + true, + ); + }); + }); + + describe("multiStepAutoSubmitLoginComplete extension message", () => { + it("removes the sender URL from the set of valid auto-submit hosts", () => { + const message = { command: "multiStepAutoSubmitLoginComplete" }; + + sendMockExtensionMessage(message, sender); + + expect(autoSubmitLoginBackground["validAutoSubmitHosts"].has(validAutoSubmitHost)).toBe( + false, + ); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.ts b/apps/browser/src/autofill/background/auto-submit-login.background.ts new file mode 100644 index 00000000000..52d4cb2b419 --- /dev/null +++ b/apps/browser/src/autofill/background/auto-submit-login.background.ts @@ -0,0 +1,648 @@ +import { firstValueFrom } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +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 { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { AutofillService } from "../services/abstractions/autofill.service"; + +import { + AutoSubmitLoginBackground as AutoSubmitLoginBackgroundAbstraction, + AutoSubmitLoginBackgroundExtensionMessageHandlers, + AutoSubmitLoginMessage, +} from "./abstractions/auto-submit-login.background"; + +export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstraction { + private validIdpHosts: Set = new Set(); + private validAutoSubmitHosts: Set = new Set(); + private mostRecentIdpHost: { url?: string; tabId?: number } = {}; + private currentAutoSubmitHostData: { url?: string; tabId?: number } = {}; + private readonly isSafariBrowser: boolean = false; + private readonly extensionMessageHandlers: AutoSubmitLoginBackgroundExtensionMessageHandlers = { + triggerAutoSubmitLogin: ({ message, sender }) => this.triggerAutoSubmitLogin(message, sender), + multiStepAutoSubmitLoginComplete: ({ sender }) => + this.handleMultiStepAutoSubmitLoginComplete(sender), + }; + + constructor( + private logService: LogService, + private autofillService: AutofillService, + private scriptInjectorService: ScriptInjectorService, + private authService: AuthService, + private configService: ConfigService, + private platformUtilsService: PlatformUtilsService, + private policyService: PolicyService, + ) { + this.isSafariBrowser = this.platformUtilsService.isSafari(); + } + + /** + * Initializes the auto-submit login policy. Will return early if + * the feature flag is not set. If the policy is not enabled, it + * will trigger a removal of any established listeners. + */ + async init() { + const featureFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.IdpAutoSubmitLogin, + ); + if (featureFlagEnabled) { + this.policyService + .get$(PolicyType.AutomaticAppLogIn) + .subscribe(this.handleAutoSubmitLoginPolicySubscription.bind(this)); + } + } + + /** + * Handles changes to the AutomaticAppLogIn policy. If the policy is not enabled, trigger + * a removal of any established listeners. If the policy is enabled, apply the policy to + * the active user. + * + * @param policy - The AutomaticAppLogIn policy details. + */ + private handleAutoSubmitLoginPolicySubscription = (policy: Policy) => { + if (!policy?.enabled) { + this.destroy(); + return; + } + + this.applyPolicyToActiveUser(policy).catch((error) => this.logService.error(error)); + }; + + /** + * Verifies if the policy applies to the active user. If so, the event listeners + * used to trigger auto-submission of login forms will be established. + * + * @param policy - The AutomaticAppLogIn policy details. + */ + private applyPolicyToActiveUser = async (policy: Policy) => { + const policyAppliesToUser = await firstValueFrom( + this.policyService.policyAppliesToActiveUser$(PolicyType.AutomaticAppLogIn), + ); + + if (!policyAppliesToUser) { + this.destroy(); + return; + } + + await this.setupAutoSubmitLoginListeners(policy); + }; + + /** + * Sets up the event listeners used to trigger auto-submission of login forms. + * + * @param policy - The AutomaticAppLogIn policy details. + */ + private setupAutoSubmitLoginListeners = async (policy: Policy) => { + this.parseIpdHostsFromPolicy(policy?.data.idpHost); + if (!this.validIdpHosts.size) { + this.destroy(); + return; + } + + BrowserApi.addListener(chrome.runtime.onMessage, this.handleExtensionMessage); + chrome.webRequest.onBeforeRequest.addListener(this.handleOnBeforeRequest, { + urls: [""], + types: ["main_frame", "sub_frame"], + }); + chrome.webRequest.onBeforeRedirect.addListener(this.handleWebRequestOnBeforeRedirect, { + urls: [""], + types: ["main_frame", "sub_frame"], + }); + + if (this.isSafariBrowser) { + this.initSafari().catch((error) => this.logService.error(error)); + } + }; + + /** + * Parses the comma-separated list of IDP hosts from the AutomaticAppLogIn policy. + * + * @param idpHost - The comma-separated list of IDP hosts. + */ + private parseIpdHostsFromPolicy = (idpHost?: string) => { + if (!idpHost) { + return; + } + + const urls = idpHost.split(","); + urls.forEach((url) => { + const host = this.getUrlHost(url?.trim()); + if (host) { + this.validIdpHosts.add(host); + } + }); + }; + + /** + * Handles the onBeforeRequest event. This event is used to determine if a request should initialize + * the auto-submit login workflow. A valid request will initialize the workflow, while an invalid + * request will clear and disable the workflow. + * + * @param details - The details of the request. + */ + private handleOnBeforeRequest = (details: chrome.webRequest.WebRequestBodyDetails) => { + const requestInitiator = this.getRequestInitiator(details); + const isValidInitiator = this.isValidInitiator(requestInitiator); + + if ( + this.postRequestEncounteredAfterSubmission(details, isValidInitiator) || + this.requestRedirectsToInvalidHost(details, isValidInitiator) + ) { + this.clearAutoSubmitHostData(); + return; + } + + if (isValidInitiator && this.shouldRouteTriggerAutoSubmit(details, requestInitiator)) { + this.setupAutoSubmitFlow(details); + return; + } + + this.disableAutoSubmitFlow(requestInitiator, details).catch((error) => + this.logService.error(error), + ); + }; + + /** + * This triggers if the upcoming request is a POST request and the initiator is valid. It indicates + * that a submission has occurred and the auto-submit login workflow should be cleared. + * + * @param details - The details of the request. + * @param isValidInitiator - A flag indicating if the initiator of the request is valid. + */ + private postRequestEncounteredAfterSubmission = ( + details: chrome.webRequest.WebRequestBodyDetails, + isValidInitiator: boolean, + ) => { + return details.method === "POST" && this.validAutoSubmitHosts.size > 0 && isValidInitiator; + }; + + /** + * Determines if a request is attempting to redirect to an invalid host. We identify this as a case + * where the top level frame has navigated to either an invalid IDP host or auto-submit host. + * + * @param details - The details of the request. + * @param isValidInitiator - A flag indicating if the initiator of the request is valid. + */ + private requestRedirectsToInvalidHost = ( + details: chrome.webRequest.WebRequestBodyDetails, + isValidInitiator: boolean, + ) => { + return ( + this.validAutoSubmitHosts.size > 0 && + this.isRequestInMainFrame(details) && + (!isValidInitiator || !this.isValidAutoSubmitHost(details.url)) + ); + }; + + /** + * Initializes the auto-submit flow for the given request, and adds the routed-to URL + * to the list of valid auto-submit hosts. + * + * @param details - The details of the request. + */ + private setupAutoSubmitFlow = (details: chrome.webRequest.WebRequestBodyDetails) => { + if (this.isRequestInMainFrame(details)) { + this.currentAutoSubmitHostData = { + url: details.url, + tabId: details.tabId, + }; + } + + const autoSubmitHost = this.getUrlHost(details.url); + this.validAutoSubmitHosts.add(autoSubmitHost); + chrome.webNavigation.onCompleted.removeListener(this.handleAutoSubmitHostNavigationCompleted); + chrome.webNavigation.onCompleted.addListener(this.handleAutoSubmitHostNavigationCompleted, { + url: [{ hostEquals: autoSubmitHost }], + }); + }; + + /** + * Triggers the injection of the auto-submit login content script once the page has completely loaded. + * + * @param details - The details of the navigation event. + */ + private handleAutoSubmitHostNavigationCompleted = ( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, + ) => { + if ( + details.tabId === this.currentAutoSubmitHostData.tabId && + this.urlContainsAutoFillParam(details.url) + ) { + this.injectAutoSubmitLoginScript(details.tabId).catch((error) => + this.logService.error(error), + ); + chrome.webNavigation.onCompleted.removeListener(this.handleAutoSubmitHostNavigationCompleted); + } + }; + + /** + * Triggers the injection of the auto-submit login script if the user is authenticated. + * + * @param tabId - The ID of the tab to inject the script into. + */ + private injectAutoSubmitLoginScript = async (tabId: number) => { + if ((await this.getAuthStatus()) === AuthenticationStatus.Unlocked) { + await this.scriptInjectorService.inject({ + tabId: tabId, + injectDetails: { + file: "content/auto-submit-login.js", + runAt: "document_start", + frame: "all_frames", + }, + }); + } + }; + + /** + * Retrieves the authentication status of the active user. + */ + private getAuthStatus = async () => { + return firstValueFrom(this.authService.activeAccountStatus$); + }; + + /** + * Handles web requests that are triggering a redirect. Stores the redirect URL as a valid + * auto-submit host if the redirectUrl should trigger an auto-submit. + * + * @param details - The details of the request. + */ + private handleWebRequestOnBeforeRedirect = ( + details: chrome.webRequest.WebRedirectionResponseDetails, + ) => { + if (this.isRequestInMainFrame(details) && this.urlContainsAutoFillParam(details.redirectUrl)) { + this.validAutoSubmitHosts.add(this.getUrlHost(details.redirectUrl)); + this.validAutoSubmitHosts.add(this.getUrlHost(details.url)); + } + }; + + /** + * Determines if the provided URL is a valid initiator for the auto-submit login feature. + * + * @param url - The URL to validate as an initiator. + */ + private isValidInitiator = (url: string) => { + return this.isValidIdpHost(url) || this.isValidAutoSubmitHost(url); + }; + + /** + * Determines if the provided URL is a valid IDP host. + * + * @param url - The URL to validate as an IDP host. + */ + private isValidIdpHost = (url: string) => { + const host = this.getUrlHost(url); + if (!host) { + return false; + } + + return this.validIdpHosts.has(host); + }; + + /** + * Determines if the provided URL is a valid auto-submit host. + * + * @param url - The URL to validate as an auto-submit host. + */ + private isValidAutoSubmitHost = (url: string) => { + const host = this.getUrlHost(url); + if (!host) { + return false; + } + + return this.validAutoSubmitHosts.has(host); + }; + + /** + * Removes the provided URL from the list of valid auto-submit hosts. + * + * @param url - The URL to remove from the list of valid auto-submit hosts. + */ + private removeUrlFromAutoSubmitHosts = (url: string) => { + this.validAutoSubmitHosts.delete(this.getUrlHost(url)); + }; + + /** + * Disables an active auto-submit login workflow. This triggers when a request is made that should + * not trigger auto-submit. If the initiator of the request is a valid auto-submit host, we need to + * treat this request as a navigation within the current website, but away from the intended + * auto-submit route. If that isn't the case, we capture the tab's details and check if an + * internal navigation is occurring. If so, we invalidate that host. + * + * @param requestInitiator - The initiator of the request. + * @param details - The details of the request. + */ + private disableAutoSubmitFlow = async ( + requestInitiator: string, + details: chrome.webRequest.WebRequestBodyDetails, + ) => { + if (this.isValidAutoSubmitHost(requestInitiator)) { + this.removeUrlFromAutoSubmitHosts(requestInitiator); + return; + } + + if (details.tabId < 0) { + return; + } + + const tab = await BrowserApi.getTab(details.tabId); + if (this.isValidAutoSubmitHost(tab?.url)) { + this.removeUrlFromAutoSubmitHosts(tab.url); + } + }; + + /** + * Clears all data associated with the current auto-submit host workflow. + */ + private clearAutoSubmitHostData = () => { + this.validAutoSubmitHosts.clear(); + this.currentAutoSubmitHostData = {}; + this.mostRecentIdpHost = {}; + }; + + /** + * Determines if the provided URL is a valid auto-submit host. If the request is occurring + * in the main frame, we will check for the presence of the `autofill=1` query parameter. + * If the request is occurring in a sub frame, the main frame URL should be set as a + * valid auto-submit host and can be used to validate the request. + * + * @param details - The details of the request. + * @param initiator - The initiator of the request. + */ + private shouldRouteTriggerAutoSubmit = ( + details: chrome.webRequest.ResourceRequest, + initiator: string, + ) => { + if (this.isRequestInMainFrame(details)) { + return !!( + this.urlContainsAutoFillParam(details.url) || + this.triggerAutoSubmitAfterRedirectOnSafari(details.url) + ); + } + + return this.isValidAutoSubmitHost(initiator); + }; + + /** + * Determines if the provided URL contains the `autofill=1` query parameter. + * + * @param url - The URL to check for the `autofill=1` query parameter. + */ + private urlContainsAutoFillParam = (url: string) => { + try { + const urlObj = new URL(url); + return urlObj.search.indexOf("autofill=1") !== -1; + } catch { + return false; + } + }; + + /** + * Extracts the host from a given URL. + * Will return an empty string if the provided URL is invalid. + * + * @param url - The URL to extract the host from. + */ + private getUrlHost = (url: string) => { + let parsedUrl = url; + if (!parsedUrl) { + return ""; + } + + if (!parsedUrl.startsWith("http")) { + parsedUrl = `https://${parsedUrl}`; + } + + try { + const urlObj = new URL(parsedUrl); + return urlObj.host; + } catch { + return ""; + } + }; + + /** + * Determines the initiator of a request. If the request is happening in a Safari browser, we + * need to determine the initiator based on the stored most recently visited IDP host. When + * handling a sub frame request in Safari, we treat the passed URL detail as the initiator + * of the request, as long as an IPD host has been previously identified. + * + * @param details - The details of the request. + */ + private getRequestInitiator = (details: chrome.webRequest.ResourceRequest) => { + if (!this.isSafariBrowser) { + return details.initiator || (details as browser.webRequest._OnBeforeRequestDetails).originUrl; + } + + if (this.isRequestInMainFrame(details)) { + return this.mostRecentIdpHost.url; + } + + if (!this.mostRecentIdpHost.url) { + return ""; + } + + return details.url; + }; + + /** + * Verifies if a request is occurring in the main / top-level frame of a tab. + * + * @param details - The details of the request. + */ + private isRequestInMainFrame = (details: chrome.webRequest.ResourceRequest) => { + if (this.isSafariBrowser) { + return details.frameId === 0; + } + + return details.type === "main_frame"; + }; + + /** + * Triggers the auto-submit login feature on the provided tab. + * + * @param message - The auto-submit login message. + * @param sender - The message sender. + */ + private triggerAutoSubmitLogin = async ( + message: AutoSubmitLoginMessage, + sender: chrome.runtime.MessageSender, + ) => { + await this.autofillService.doAutoFillOnTab( + [ + { + frameId: sender.frameId, + tab: sender.tab, + details: message.pageDetails, + }, + ], + sender.tab, + true, + true, + ); + }; + + /** + * Handles the completion of auto-submit login workflow on a multistep form. + * + * @param sender - The message sender. + */ + private handleMultiStepAutoSubmitLoginComplete = (sender: chrome.runtime.MessageSender) => { + this.removeUrlFromAutoSubmitHosts(sender.url); + }; + + /** + * Initializes several fallback event listeners for the auto-submit login feature on the Safari browser. + * This is required due to limitations that Safari has with the `webRequest` API. Specifically, Safari + * does not provide the `initiator` of a request, which is required to determine if a request is coming + * from a valid IDP host. + */ + private async initSafari() { + const currentTab = await BrowserApi.getTabFromCurrentWindow(); + if (currentTab) { + this.setMostRecentIdpHost(currentTab.url, currentTab.id); + } + + chrome.tabs.onActivated.addListener(this.handleSafariTabOnActivated); + chrome.tabs.onUpdated.addListener(this.handleSafariTabOnUpdated); + chrome.webNavigation.onCompleted.addListener(this.handleSafariWebNavigationOnCompleted); + } + + /** + * Sets the most recent IDP host based on the provided URL and tab ID. + * + * @param url - The URL to set as the most recent IDP host. + * @param tabId - The tab ID associated with the URL. + */ + private setMostRecentIdpHost(url: string, tabId: number) { + if (this.isValidIdpHost(url)) { + this.mostRecentIdpHost = { url, tabId }; + } + } + + /** + * Triggers an update of the most recently visited IDP host when a user focuses a different tab. + * + * @param activeInfo - The active tab information. + */ + private handleSafariTabOnActivated = async (activeInfo: chrome.tabs.TabActiveInfo) => { + if (activeInfo.tabId < 0) { + return; + } + + const tab = await BrowserApi.getTab(activeInfo.tabId); + if (tab) { + this.setMostRecentIdpHost(tab.url, tab.id); + } + }; + + /** + * Triggers an update of the most recently visited IDP host when the URL of a tab is updated. + * + * @param tabId - The tab ID associated with the URL. + * @param changeInfo - The change information of the tab. + */ + private handleSafariTabOnUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (changeInfo) { + this.setMostRecentIdpHost(changeInfo.url, tabId); + } + }; + + /** + * Handles the completion of a web navigation event on the Safari browser. If the navigation event + * is for the main frame and the URL is a valid IDP host, the most recent IDP host will be updated. + * + * @param details - The web navigation details. + */ + private handleSafariWebNavigationOnCompleted = ( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, + ) => { + if (details.frameId === 0 && this.isValidIdpHost(details.url)) { + this.validAutoSubmitHosts.clear(); + this.mostRecentIdpHost = { + url: details.url, + tabId: details.tabId, + }; + chrome.tabs.onRemoved.addListener(this.handleSafariTabOnRemoved); + } + }; + + /** + * Handles the removal of a tab on the Safari browser. If the tab being removed is the current + * auto-submit host tab, all data associated with the current auto-submit workflow will be cleared. + * + * @param tabId - The tab ID of the tab being removed. + */ + private handleSafariTabOnRemoved = (tabId: number) => { + if (this.currentAutoSubmitHostData.tabId === tabId) { + this.clearAutoSubmitHostData(); + chrome.tabs.onRemoved.removeListener(this.handleSafariTabOnRemoved); + } + }; + + /** + * Determines if the auto-submit login feature should be triggered after a redirect on the Safari browser. + * This is required because Safari does not provide query params for the URL that is being routed to within + * the onBefore request listener. + * + * @param url - The URL of the redirect. + */ + private triggerAutoSubmitAfterRedirectOnSafari = (url: string) => { + return this.isSafariBrowser && this.isValidAutoSubmitHost(url); + }; + + /** + * Handles incoming messages from the extension. The message is only listened to if it comes from + * the current auto-submit workflow tab and the URL is a valid auto-submit host. + * + * @param message - The incoming message. + * @param sender - The message sender. + * @param sendResponse - The response callback. + */ + private handleExtensionMessage = async ( + message: AutoSubmitLoginMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const { tab, url } = sender; + if (tab?.id !== this.currentAutoSubmitHostData.tabId || !this.isValidAutoSubmitHost(url)) { + return null; + } + + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch((error) => this.logService.error(error)); + return true; + }; + + /** + * Tears down all established event listeners for the auto-submit login feature. + */ + private destroy() { + BrowserApi.removeListener(chrome.runtime.onMessage, this.handleExtensionMessage); + chrome.webRequest.onBeforeRequest.removeListener(this.handleOnBeforeRequest); + chrome.webRequest.onBeforeRedirect.removeListener(this.handleWebRequestOnBeforeRedirect); + chrome.webNavigation.onCompleted.removeListener(this.handleAutoSubmitHostNavigationCompleted); + chrome.webNavigation.onCompleted.removeListener(this.handleSafariWebNavigationOnCompleted); + chrome.tabs.onActivated.removeListener(this.handleSafariTabOnActivated); + chrome.tabs.onUpdated.removeListener(this.handleSafariTabOnUpdated); + chrome.tabs.onRemoved.removeListener(this.handleSafariTabOnRemoved); + } +} diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index fe118868628..2ca0920a38f 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -13,10 +13,12 @@ import { DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { EnvironmentService, Region, } from "@bitwarden/common/platform/abstractions/environment.service"; +import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; @@ -32,6 +34,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; @@ -73,6 +76,7 @@ describe("OverlayBackground", () => { let accountService: FakeAccountService; let fakeStateProvider: FakeStateProvider; let showFaviconsMock$: BehaviorSubject; + let neverDomainsMock$: BehaviorSubject; let domainSettingsService: DomainSettingsService; let logService: MockProxy; let cipherService: MockProxy; @@ -85,6 +89,8 @@ describe("OverlayBackground", () => { let autofillSettingsService: MockProxy; let i18nService: MockProxy; let platformUtilsService: MockProxy; + let availableAutofillCredentialsMock$: BehaviorSubject; + let fido2ClientService: MockProxy; let selectedThemeMock$: BehaviorSubject; let themeStateService: MockProxy; let overlayBackground: OverlayBackground; @@ -129,8 +135,10 @@ describe("OverlayBackground", () => { accountService = mockAccountServiceWith(mockUserId); fakeStateProvider = new FakeStateProvider(accountService); showFaviconsMock$ = new BehaviorSubject(true); + neverDomainsMock$ = new BehaviorSubject({}); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); domainSettingsService.showFavicons$ = showFaviconsMock$; + domainSettingsService.neverDomains$ = neverDomainsMock$; logService = mock(); cipherService = mock(); autofillService = mock(); @@ -151,6 +159,10 @@ describe("OverlayBackground", () => { autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; i18nService = mock(); platformUtilsService = mock(); + availableAutofillCredentialsMock$ = new BehaviorSubject([]); + fido2ClientService = mock({ + availableAutofillCredentials$: (_tabId) => availableAutofillCredentialsMock$, + }); selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); themeStateService = mock(); themeStateService.selectedTheme$ = selectedThemeMock$; @@ -164,6 +176,7 @@ describe("OverlayBackground", () => { autofillSettingsService, i18nService, platformUtilsService, + fido2ClientService, themeStateService, ); portKeyForTabSpy = overlayBackground["portKeyForTab"]; @@ -699,28 +712,28 @@ describe("OverlayBackground", () => { describe("updating the overlay ciphers", () => { const url = "https://jest-testing-website.com"; const tab = createChromeTabMock({ url }); - const cipher1 = mock({ + const loginCipher1 = mock({ id: "id-1", localData: { lastUsedDate: 222 }, name: "name-1", type: CipherType.Login, login: { username: "username-1", uri: url }, }); - const cipher2 = mock({ + const cardCipher = mock({ id: "id-2", localData: { lastUsedDate: 222 }, name: "name-2", type: CipherType.Card, card: { subTitle: "subtitle-2" }, }); - const cipher3 = mock({ + const loginCipher2 = mock({ id: "id-3", localData: { lastUsedDate: 222 }, name: "name-3", type: CipherType.Login, login: { username: "username-3", uri: url }, }); - const cipher4 = mock({ + const identityCipher = mock({ id: "id-4", localData: { lastUsedDate: 222 }, name: "name-4", @@ -732,6 +745,24 @@ describe("OverlayBackground", () => { email: "email@example.com", }, }); + const passkeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + uri: url, + fido2Credentials: [ + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + ], + }, + }); beforeEach(async () => { activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); @@ -764,7 +795,7 @@ describe("OverlayBackground", () => { it("closes the inline menu on the focused field's tab if current tab is different", async () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); const previousTab = mock({ id: 15 }); overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 15 }); @@ -781,7 +812,7 @@ describe("OverlayBackground", () => { it("queries all cipher types, sorts them by last used, and formats them for usage in the overlay", async () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(); @@ -794,8 +825,8 @@ describe("OverlayBackground", () => { expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["inline-menu-cipher-0", cipher2], - ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-0", cardCipher], + ["inline-menu-cipher-1", loginCipher1], ]), ); }); @@ -803,7 +834,7 @@ describe("OverlayBackground", () => { it("queries only login ciphers when not updating all cipher types", async () => { overlayBackground["cardAndIdentityCiphers"] = new Set([]); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher3, cipher1]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher2, loginCipher1]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(false); @@ -813,15 +844,15 @@ describe("OverlayBackground", () => { expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["inline-menu-cipher-0", cipher1], - ["inline-menu-cipher-1", cipher3], + ["inline-menu-cipher-0", loginCipher1], + ["inline-menu-cipher-1", loginCipher2], ]), ); }); it("queries all cipher types when the card and identity ciphers set is not built when only updating login ciphers", async () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(false); @@ -834,15 +865,15 @@ describe("OverlayBackground", () => { expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["inline-menu-cipher-0", cipher2], - ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-0", cardCipher], + ["inline-menu-cipher-1", loginCipher1], ]), ); }); it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -851,10 +882,11 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: false, + showPasskeysLabels: false, ciphers: [ { accountCreationFieldType: undefined, - favorite: cipher1.favorite, + favorite: loginCipher1.favorite, icon: { fallbackImage: "images/bwi-globe.png", icon: "bwi-globe", @@ -864,9 +896,10 @@ describe("OverlayBackground", () => { id: "inline-menu-cipher-0", login: { username: "username-1", + passkey: null, }, name: "name-1", - reprompt: cipher1.reprompt, + reprompt: loginCipher1.reprompt, type: CipherType.Login, }, ], @@ -878,7 +911,7 @@ describe("OverlayBackground", () => { tabId: tab.id, filledByCipherType: CipherType.Card, }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -887,10 +920,11 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: false, + showPasskeysLabels: false, ciphers: [ { accountCreationFieldType: undefined, - favorite: cipher2.favorite, + favorite: cardCipher.favorite, icon: { fallbackImage: "", icon: "bwi-credit-card", @@ -898,9 +932,9 @@ describe("OverlayBackground", () => { imageEnabled: true, }, id: "inline-menu-cipher-0", - card: cipher2.card.subTitle, - name: cipher2.name, - reprompt: cipher2.reprompt, + card: cardCipher.card.subTitle, + name: cardCipher.name, + reprompt: cardCipher.reprompt, type: CipherType.Card, }, ], @@ -914,7 +948,7 @@ describe("OverlayBackground", () => { accountCreationFieldType: "text", showInlineMenuAccountCreation: true, }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4, cipher2]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -923,10 +957,11 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, + showPasskeysLabels: false, ciphers: [ { accountCreationFieldType: "text", - favorite: cipher4.favorite, + favorite: identityCipher.favorite, icon: { fallbackImage: "", icon: "bwi-id-card", @@ -934,12 +969,12 @@ describe("OverlayBackground", () => { imageEnabled: true, }, id: "inline-menu-cipher-1", - name: cipher4.name, - reprompt: cipher4.reprompt, + name: identityCipher.name, + reprompt: identityCipher.reprompt, type: CipherType.Identity, identity: { - fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`, - username: cipher4.identity.username, + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.username, }, }, ], @@ -952,7 +987,7 @@ describe("OverlayBackground", () => { accountCreationFieldType: "text", showInlineMenuAccountCreation: true, }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher4]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, identityCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -961,10 +996,11 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, + showPasskeysLabels: false, ciphers: [ { accountCreationFieldType: "text", - favorite: cipher4.favorite, + favorite: identityCipher.favorite, icon: { fallbackImage: "", icon: "bwi-id-card", @@ -972,17 +1008,17 @@ describe("OverlayBackground", () => { imageEnabled: true, }, id: "inline-menu-cipher-0", - name: cipher4.name, - reprompt: cipher4.reprompt, + name: identityCipher.name, + reprompt: identityCipher.reprompt, type: CipherType.Identity, identity: { - fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`, - username: cipher4.identity.username, + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.username, }, }, { accountCreationFieldType: "text", - favorite: cipher1.favorite, + favorite: loginCipher1.favorite, icon: { fallbackImage: "images/bwi-globe.png", icon: "bwi-globe", @@ -991,10 +1027,11 @@ describe("OverlayBackground", () => { }, id: "inline-menu-cipher-1", login: { - username: cipher1.login.username, + username: loginCipher1.login.username, + passkey: null, }, - name: cipher1.name, - reprompt: cipher1.reprompt, + name: loginCipher1.name, + reprompt: loginCipher1.reprompt, type: CipherType.Login, }, ], @@ -1018,7 +1055,7 @@ describe("OverlayBackground", () => { }, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([ - cipher4, + identityCipher, identityCipherWithoutUsername, ]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); @@ -1029,10 +1066,11 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, + showPasskeysLabels: false, ciphers: [ { accountCreationFieldType: "email", - favorite: cipher4.favorite, + favorite: identityCipher.favorite, icon: { fallbackImage: "", icon: "bwi-id-card", @@ -1040,12 +1078,12 @@ describe("OverlayBackground", () => { imageEnabled: true, }, id: "inline-menu-cipher-1", - name: cipher4.name, - reprompt: cipher4.reprompt, + name: identityCipher.name, + reprompt: identityCipher.reprompt, type: CipherType.Identity, identity: { - fullName: `${cipher4.identity.firstName} ${cipher4.identity.lastName}`, - username: cipher4.identity.email, + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.email, }, }, ], @@ -1058,7 +1096,7 @@ describe("OverlayBackground", () => { accountCreationFieldType: "password", showInlineMenuAccountCreation: true, }); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher4]); + cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); @@ -1067,10 +1105,147 @@ describe("OverlayBackground", () => { expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, + showPasskeysLabels: false, ciphers: [], }); }); }); + + it("adds available passkey ciphers to the inline menu", async () => { + availableAutofillCredentialsMock$.next(passkeyCipher.login.fido2Credentials); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Login, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + ciphers: [ + { + id: "inline-menu-cipher-0", + name: passkeyCipher.name, + type: CipherType.Login, + reprompt: passkeyCipher.reprompt, + favorite: passkeyCipher.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: passkeyCipher.login.username, + passkey: { + rpName: passkeyCipher.login.fido2Credentials[0].rpName, + userName: passkeyCipher.login.fido2Credentials[0].userName, + }, + }, + }, + { + id: "inline-menu-cipher-0", + name: passkeyCipher.name, + type: CipherType.Login, + reprompt: passkeyCipher.reprompt, + favorite: passkeyCipher.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: passkeyCipher.login.username, + passkey: null, + }, + }, + { + id: "inline-menu-cipher-1", + name: loginCipher1.name, + type: CipherType.Login, + reprompt: loginCipher1.reprompt, + favorite: loginCipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: loginCipher1.login.username, + passkey: null, + }, + }, + ], + showInlineMenuAccountCreation: false, + showPasskeysLabels: true, + }); + }); + + it("does not add a passkey to the inline menu when its rpId is part of the neverDomains exclusion list", async () => { + availableAutofillCredentialsMock$.next(passkeyCipher.login.fido2Credentials); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Login, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + neverDomainsMock$.next({ "jest-testing-website.com": null }); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + ciphers: [ + { + id: "inline-menu-cipher-0", + name: passkeyCipher.name, + type: CipherType.Login, + reprompt: passkeyCipher.reprompt, + favorite: passkeyCipher.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: passkeyCipher.login.username, + passkey: null, + }, + }, + { + id: "inline-menu-cipher-1", + name: loginCipher1.name, + type: CipherType.Login, + reprompt: loginCipher1.reprompt, + favorite: loginCipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: loginCipher1.login.username, + passkey: null, + }, + }, + ], + showInlineMenuAccountCreation: false, + showPasskeysLabels: false, + }); + }); }); describe("extension message handlers", () => { @@ -1562,6 +1737,7 @@ describe("OverlayBackground", () => { command: "updateAutofillInlineMenuListCiphers", ciphers: [], showInlineMenuAccountCreation: true, + showPasskeysLabels: false, }); }); @@ -2660,6 +2836,41 @@ describe("OverlayBackground", () => { expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); }); + + it("triggers passkey authentication through mediated conditional UI", async () => { + const fido2Credential = mock({ credentialId: "credential-id" }); + const cipher1 = mock({ + id: "inline-menu-cipher-1", + login: { + username: "username1", + password: "password1", + fido2Credentials: [fido2Credential], + }, + }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + usePasskey: true, + portKey, + }); + await flushPromises(); + + expect(fido2ClientService.autofillCredential).toHaveBeenCalledWith( + sender.tab.id, + fido2Credential.credentialId, + ); + }); }); describe("addNewVaultItem message handler", () => { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 8c4dac56d50..2cef0b0e788 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,5 +1,13 @@ -import { firstValueFrom, merge, Subject, throttleTime } from "rxjs"; -import { debounceTime, switchMap } from "rxjs/operators"; +import { + firstValueFrom, + merge, + ReplaySubject, + Subject, + throttleTime, + switchMap, + debounceTime, +} from "rxjs"; +import { parse } from "tldts"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -10,7 +18,9 @@ import { import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { Fido2ClientService } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; 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"; @@ -21,6 +31,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; @@ -41,6 +52,7 @@ import { generateRandomChars } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { + BuildCipherDataParams, CloseInlineMenuMessage, CurrentAddNewItemData, FocusedFieldData, @@ -66,6 +78,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private readonly storeInlineMenuFido2CredentialsSubject = new ReplaySubject(1); private pageDetailsForTab: PageDetailsForTab = {}; private subFrameOffsetsForTab: SubFrameOffsetsForTab = {}; private portKeyForTab: Record = {}; @@ -73,6 +86,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private inlineMenuButtonPort: chrome.runtime.Port; private inlineMenuListPort: chrome.runtime.Port; private inlineMenuCiphers: Map = new Map(); + private inlineMenuFido2Credentials: Set = new Set(); private inlineMenuPageTranslations: Record; private inlineMenuPosition: InlineMenuPosition = {}; private cardAndIdentityCiphers: Set | null = null; @@ -91,6 +105,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private isFieldCurrentlyFilling: boolean = false; private isInlineMenuButtonVisible: boolean = false; private isInlineMenuListVisible: boolean = false; + private showPasskeysLabelsWithinInlineMenu: boolean = false; private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { autofillOverlayElementClosed: ({ message, sender }) => @@ -159,6 +174,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private fido2ClientService: Fido2ClientService, private themeStateService: ThemeStateService, ) { this.initOverlayEventObservables(); @@ -178,6 +194,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Initializes event observables that handle events which affect the overlay's behavior. */ private initOverlayEventObservables() { + this.storeInlineMenuFido2CredentialsSubject + .pipe(switchMap((tabId) => this.fido2ClientService.availableAutofillCredentials$(tabId))) + .subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials)); this.repositionInlineMenuSubject .pipe( debounceTime(1000), @@ -252,6 +271,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); } + if (!currentTab) { + return; + } + + this.inlineMenuFido2Credentials.clear(); + this.storeInlineMenuFido2CredentialsSubject.next(currentTab.id); + this.inlineMenuCiphers = new Map(); const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { @@ -263,6 +289,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { command: "updateAutofillInlineMenuListCiphers", ciphers, showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, }); } @@ -280,9 +307,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { return this.getAllCipherTypeViews(currentTab); } - const cipherViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab?.url || "") - ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); + const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort( + (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + ); return this.cardAndIdentityCiphers ? cipherViews.concat(...this.cardAndIdentityCiphers) @@ -301,7 +328,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.cardAndIdentityCiphers.clear(); const cipherViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab.url, [ + await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [ CipherType.Card, CipherType.Identity, ]) @@ -331,6 +358,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); const inlineMenuCiphersArray = Array.from(this.inlineMenuCiphers); let inlineMenuCipherData: InlineMenuCipherData[]; + this.showPasskeysLabelsWithinInlineMenu = false; if (this.showInlineMenuAccountCreation()) { inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers( @@ -338,7 +366,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { true, ); } else { - inlineMenuCipherData = this.buildInlineMenuCiphers(inlineMenuCiphersArray, showFavicons); + inlineMenuCipherData = await this.buildInlineMenuCiphers( + inlineMenuCiphersArray, + showFavicons, + ); } this.currentInlineMenuCiphersCount = inlineMenuCipherData.length; @@ -363,7 +394,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (cipher.type === CipherType.Login) { accountCreationLoginCiphers.push( - this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true), + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + showInlineMenuAccountCreation: true, + }), ); continue; } @@ -378,7 +414,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { } inlineMenuCipherData.push( - this.buildCipherData(inlineMenuCipherId, cipher, showFavicons, true, identity), + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + showInlineMenuAccountCreation: true, + identityData: identity, + }), ); } @@ -395,11 +437,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param inlineMenuCiphersArray - Array of inline menu ciphers * @param showFavicons - Identifies whether favicons should be shown */ - private buildInlineMenuCiphers( + private async buildInlineMenuCiphers( inlineMenuCiphersArray: [string, CipherView][], showFavicons: boolean, ) { const inlineMenuCipherData: InlineMenuCipherData[] = []; + const passkeyCipherData: InlineMenuCipherData[] = []; + const domainExclusions = await this.getExcludedDomains(); + let domainExclusionsSet: Set | null = null; + if (domainExclusions) { + domainExclusionsSet = new Set(Object.keys(await this.getExcludedDomains())); + } for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; @@ -407,12 +455,58 @@ export class OverlayBackground implements OverlayBackgroundInterface { continue; } - inlineMenuCipherData.push(this.buildCipherData(inlineMenuCipherId, cipher, showFavicons)); + if (this.showCipherAsPasskey(cipher, domainExclusionsSet)) { + passkeyCipherData.push( + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + hasPasskey: true, + }), + ); + } + + inlineMenuCipherData.push(this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons })); + } + + if (passkeyCipherData.length) { + this.showPasskeysLabelsWithinInlineMenu = + passkeyCipherData.length > 0 && inlineMenuCipherData.length > 0; + return passkeyCipherData.concat(inlineMenuCipherData); } return inlineMenuCipherData; } + /** + * Identifies whether we should show the cipher as a passkey in the inline menu list. + * + * @param cipher - The cipher to check + * @param domainExclusions - The domain exclusions to check against + */ + private showCipherAsPasskey(cipher: CipherView, domainExclusions: Set | null): boolean { + if (cipher.type !== CipherType.Login) { + return false; + } + + const fido2Credentials = cipher.login.fido2Credentials; + if (!fido2Credentials?.length) { + return false; + } + + const credentialId = fido2Credentials[0].credentialId; + const rpId = fido2Credentials[0].rpId; + const parsedRpId = parse(rpId, { allowPrivateDomains: true }); + if (domainExclusions?.has(parsedRpId.domain)) { + return false; + } + + return ( + this.inlineMenuFido2Credentials.size === 0 || + this.inlineMenuFido2Credentials.has(credentialId) + ); + } + /** * Builds the cipher data for the inline menu list. * @@ -420,15 +514,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param cipher - The cipher to build data for * @param showFavicons - Identifies whether favicons should be shown * @param showInlineMenuAccountCreation - Identifies whether the inline menu is for account creation + * @param hasPasskey - Identifies whether the cipher has a FIDO2 credential * @param identityData - Pre-created identity data */ - private buildCipherData( - inlineMenuCipherId: string, - cipher: CipherView, - showFavicons: boolean, - showInlineMenuAccountCreation: boolean = false, - identityData?: { fullName: string; username?: string }, - ): InlineMenuCipherData { + private buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + showInlineMenuAccountCreation, + hasPasskey, + identityData, + }: BuildCipherDataParams): InlineMenuCipherData { const inlineMenuData: InlineMenuCipherData = { id: inlineMenuCipherId, name: cipher.name, @@ -440,7 +536,15 @@ export class OverlayBackground implements OverlayBackgroundInterface { }; if (cipher.type === CipherType.Login) { - inlineMenuData.login = { username: cipher.login.username }; + inlineMenuData.login = { + username: cipher.login.username, + passkey: hasPasskey + ? { + rpName: cipher.login.fido2Credentials[0].rpName, + userName: cipher.login.fido2Credentials[0].userName, + } + : null, + }; return inlineMenuData; } @@ -512,6 +616,25 @@ export class OverlayBackground implements OverlayBackgroundInterface { return this.inlineMenuCiphers.size === 0; } + /** + * Stores the credential ids associated with a FIDO2 conditional mediated ui request. + * + * @param credentials - The FIDO2 credentials to store + */ + private storeInlineMenuFido2Credentials(credentials: Fido2CredentialView[]) { + credentials.forEach( + (credential) => + credential?.credentialId && this.inlineMenuFido2Credentials.add(credential.credentialId), + ); + } + + /** + * Gets the neverDomains setting from the domain settings service. + */ + async getExcludedDomains(): Promise { + return await firstValueFrom(this.domainSettingsService.neverDomains$); + } + /** * Gets the currently focused field and closes the inline menu on that tab. */ @@ -749,10 +872,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { * the selected cipher at the top of the list of ciphers. * * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. + * @param usePasskey - Identifies whether the cipher has a FIDO2 credential * @param sender - The sender of the port message */ private async fillInlineMenuCipher( - { inlineMenuCipherId }: OverlayPortMessage, + { inlineMenuCipherId, usePasskey }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { const pageDetails = this.pageDetailsForTab[sender.tab.id]; @@ -762,6 +886,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); + if (usePasskey && cipher.login?.hasFido2Credentials) { + await this.fido2ClientService.autofillCredential( + sender.tab.id, + cipher.login.fido2Credentials[0].credentialId, + ); + this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); + + return; + } + if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { return; } @@ -777,6 +911,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.platformUtilsService.copyToClipboard(totpCode); } + this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); + } + + /** + * Sets the most recently used cipher at the top of the list of ciphers. + * + * @param inlineMenuCipherId - The ID of the inline menu cipher + * @param cipher - The cipher to set as the most recently used + */ + private updateLastUsedInlineMenuCipher(inlineMenuCipherId: string, cipher: CipherView) { this.inlineMenuCiphers = new Map([[inlineMenuCipherId, cipher], ...this.inlineMenuCiphers]); } @@ -1163,6 +1307,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { command: "updateAutofillInlineMenuListCiphers", ciphers: await this.getInlineMenuCipherData(), showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, }); } @@ -1214,6 +1359,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) { this.clearDelayedInlineMenuClosure(); const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (!currentTab) { + return; + } await BrowserApi.tabSendMessage( currentTab, @@ -1224,8 +1372,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { authStatus: await this.getAuthStatus(), }, { - frameId: - this.focusedFieldData?.tabId === currentTab?.id ? this.focusedFieldData.frameId : 0, + frameId: this.focusedFieldData?.tabId === currentTab.id ? this.focusedFieldData.frameId : 0, }, ); } @@ -1367,6 +1514,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { newIdentity: this.i18nService.translate("newIdentity"), addNewIdentityItem: this.i18nService.translate("addNewIdentityItemAria"), cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"), + passkeys: this.i18nService.translate("passkeys"), + passwords: this.i18nService.translate("passwords"), + logInWithPasskey: this.i18nService.translate("logInWithPasskeyAriaLabel"), }; } @@ -2064,6 +2214,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { : AutofillOverlayPort.ButtonMessageConnector, filledByCipherType: this.focusedFieldData?.filledByCipherType, showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, }); this.updateInlineMenuPosition( { diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts new file mode 100644 index 00000000000..c5bc9dbccbd --- /dev/null +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -0,0 +1,327 @@ +import AutofillPageDetails from "../models/autofill-page-details"; +import AutofillScript from "../models/autofill-script"; +import { + createAutofillFieldMock, + createAutofillPageDetailsMock, + createAutofillScriptMock, +} from "../spec/autofill-mocks"; +import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils"; +import { FormFieldElement } from "../types"; + +let pageDetailsMock: AutofillPageDetails; +let fillScriptMock: AutofillScript; +let autofillFieldElementByOpidMock: FormFieldElement; + +jest.mock("../services/collect-autofill-content.service", () => { + const module = jest.requireActual("../services/collect-autofill-content.service"); + return { + CollectAutofillContentService: class extends module.CollectAutofillContentService { + async getPageDetails(): Promise { + return pageDetailsMock; + } + + deepQueryElements(element: HTMLElement, queryString: string): T[] { + return Array.from(element.querySelectorAll(queryString)) as T[]; + } + + getAutofillFieldElementByOpid(opid: string) { + const mockedEl = autofillFieldElementByOpidMock; + if (mockedEl) { + autofillFieldElementByOpidMock = null; + return mockedEl; + } + + return Array.from(document.querySelectorAll(`*`)).find( + (el) => (el as any).opid === opid, + ) as FormFieldElement; + } + }, + }; +}); +jest.mock("../services/insert-autofill-content.service"); + +describe("AutoSubmitLogin content script", () => { + beforeEach(() => { + jest.useFakeTimers(); + setupEnvironmentDefaults(); + require("./auto-submit-login"); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllTimers(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it("ends the auto-submit login workflow if the page does not contain any fields", async () => { + pageDetailsMock.fields = []; + + await initAutoSubmitWorkflow(); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + }); + + describe("when the page contains form fields", () => { + it("ends the auto-submit login workflow if the provided fill script does not contain an autosubmit value", async () => { + await initAutoSubmitWorkflow(); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + }); + + describe("triggering auto-submit on formless fields", () => { + beforeEach(async () => { + pageDetailsMock.fields = [ + createAutofillFieldMock({ htmlID: "username", formOpid: null, opid: "name-field" }), + createAutofillFieldMock({ + htmlID: "password", + type: "password", + formOpid: null, + opid: "password-field", + }), + ]; + fillScriptMock = createAutofillScriptMock( + { + autosubmit: [null], + }, + { "name-field": "name-value", "password-field": "password-value" }, + ); + document.body.innerHTML = ` +
+
+ + +
+
+ + +
+
+
+ +
+ `; + const passwordElement = document.getElementById("password") as HTMLInputElement; + (passwordElement as any).opid = "password-field"; + await initAutoSubmitWorkflow(); + }); + + it("triggers the submit action on an element that contains a type=Submit attribute", async () => { + const submitButton = document.querySelector( + ".submit-container input[type=submit]", + ) as HTMLInputElement; + jest.spyOn(submitButton, "click"); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(submitButton.click).toHaveBeenCalled(); + }); + + it("triggers the submit action on a button element if a type=Submit element does not exist", async () => { + const submitButton = document.createElement("button"); + submitButton.innerHTML = "Submit"; + const submitContainer = document.querySelector(".submit-container"); + submitContainer.innerHTML = ""; + submitContainer.appendChild(submitButton); + jest.spyOn(submitButton, "click"); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(submitButton.click).toHaveBeenCalled(); + }); + + it("triggers the submit action when the field is within a shadow root", async () => { + createFormlessShadowRootFields(); + const submitButton = document.querySelector("input[type=submit]") as HTMLInputElement; + jest.spyOn(submitButton, "click"); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(submitButton.click).toHaveBeenCalled(); + }); + }); + + describe("triggering auto-submit on a form", () => { + beforeEach(async () => { + pageDetailsMock.fields = [ + createAutofillFieldMock({ + htmlID: "username", + formOpid: "__form0__", + opid: "name-field", + }), + createAutofillFieldMock({ + htmlID: "password", + type: "password", + formOpid: "__form0__", + opid: "password-field", + }), + ]; + fillScriptMock = createAutofillScriptMock( + { + autosubmit: ["__form0__"], + }, + { "name-field": "name-value", "password-field": "password-value" }, + ); + document.body.innerHTML = ` +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ `; + const formElement = document.querySelector("form") as HTMLFormElement; + (formElement as any).opid = "__form0__"; + formElement.addEventListener("submit", (e) => e.preventDefault()); + const passwordElement = document.getElementById("password") as HTMLInputElement; + (passwordElement as any).opid = "password-field"; + await initAutoSubmitWorkflow(); + }); + + it("attempts to trigger submission of the element as a formless field if the form cannot be found by opid", async () => { + const formElement = document.querySelector("form") as HTMLFormElement; + (formElement as any).opid = "__form1__"; + const submitButton = document.querySelector( + ".submit-container input[type=submit]", + ) as HTMLInputElement; + jest.spyOn(submitButton, "click"); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(submitButton.click).toHaveBeenCalled(); + }); + + it("triggers the submit action on an element that contains a type=Submit attribute", async () => { + const submitButton = document.querySelector( + ".submit-container input[type=submit]", + ) as HTMLInputElement; + jest.spyOn(submitButton, "click"); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(submitButton.click).toHaveBeenCalled(); + }); + + it("triggers the form's requestSubmit method when the form does not contain an button to allow submission", async () => { + const submitButton = document.querySelector( + ".submit-container input[type=submit]", + ) as HTMLInputElement; + submitButton.remove(); + const formElement = document.querySelector("form") as HTMLFormElement; + jest.spyOn(formElement, "requestSubmit").mockImplementation(); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(formElement.requestSubmit).toHaveBeenCalled(); + }); + + it("triggers the form's submit method when the requestSubmit method is not available", async () => { + const submitButton = document.querySelector( + ".submit-container input[type=submit]", + ) as HTMLInputElement; + submitButton.remove(); + const formElement = document.querySelector("form") as HTMLFormElement; + formElement.requestSubmit = undefined; + jest.spyOn(formElement, "submit").mockImplementation(); + + sendMockExtensionMessage({ + command: "triggerAutoSubmitLogin", + fillScript: fillScriptMock, + pageDetailsUrl: globalThis.location.href, + }); + await flushPromises(); + + expect(formElement.submit).toHaveBeenCalled(); + }); + }); + }); +}); + +function setupEnvironmentDefaults() { + document.body.innerHTML = ``; + pageDetailsMock = createAutofillPageDetailsMock(); + fillScriptMock = createAutofillScriptMock(); +} + +async function initAutoSubmitWorkflow() { + jest.advanceTimersByTime(250); + await flushPromises(); +} + +function createFormlessShadowRootFields() { + document.body.innerHTML = ``; + const wrapper = document.createElement("div"); + const usernameShadowRoot = document.createElement("div"); + usernameShadowRoot.attachShadow({ mode: "open" }); + usernameShadowRoot.shadowRoot.innerHTML = ``; + const passwordShadowRoot = document.createElement("div"); + passwordShadowRoot.attachShadow({ mode: "open" }); + const passwordElement = document.createElement("input"); + passwordElement.type = "password"; + passwordElement.id = "password"; + passwordElement.name = "password"; + (passwordElement as any).opid = "password-field"; + autofillFieldElementByOpidMock = passwordElement; + passwordShadowRoot.shadowRoot.appendChild(passwordElement); + const normalSubmitButton = document.createElement("input"); + normalSubmitButton.type = "submit"; + + wrapper.appendChild(usernameShadowRoot); + wrapper.appendChild(passwordShadowRoot); + wrapper.appendChild(normalSubmitButton); + document.body.appendChild(wrapper); +} diff --git a/apps/browser/src/autofill/content/auto-submit-login.ts b/apps/browser/src/autofill/content/auto-submit-login.ts new file mode 100644 index 00000000000..9cc06f874e6 --- /dev/null +++ b/apps/browser/src/autofill/content/auto-submit-login.ts @@ -0,0 +1,328 @@ +import { EVENTS } from "@bitwarden/common/autofill/constants"; + +import AutofillPageDetails from "../models/autofill-page-details"; +import AutofillScript from "../models/autofill-script"; +import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; +import InsertAutofillContentService from "../services/insert-autofill-content.service"; +import { elementIsInputElement, nodeIsFormElement, sendExtensionMessage } from "../utils"; + +(function (globalContext) { + const domElementVisibilityService = new DomElementVisibilityService(); + const collectAutofillContentService = new CollectAutofillContentService( + domElementVisibilityService, + ); + const insertAutofillContentService = new InsertAutofillContentService( + domElementVisibilityService, + collectAutofillContentService, + ); + const loginKeywords = [ + "login", + "log in", + "log-in", + "signin", + "sign in", + "sign-in", + "submit", + "continue", + "next", + ]; + let autoSubmitLoginTimeout: number | NodeJS.Timeout; + + init(); + + /** + * Initializes the auto-submit workflow with a delay to ensure that all page content is loaded. + */ + function init() { + const triggerOnPageLoad = () => setAutoSubmitLoginTimeout(250); + if (globalContext.document.readyState === "complete") { + triggerOnPageLoad(); + return; + } + + globalContext.document.addEventListener(EVENTS.DOMCONTENTLOADED, triggerOnPageLoad); + } + + /** + * Collects the autofill page details and triggers the auto-submit login workflow. + * If no details are found, we exit the auto-submit workflow. + */ + async function startAutoSubmitLoginWorkflow() { + const pageDetails: AutofillPageDetails = await collectAutofillContentService.getPageDetails(); + if (!pageDetails?.fields.length) { + endUpAutoSubmitLoginWorkflow(); + return; + } + + chrome.runtime.onMessage.addListener(handleExtensionMessage); + await sendExtensionMessage("triggerAutoSubmitLogin", { pageDetails }); + } + + /** + * Ends the auto-submit login workflow. + */ + function endUpAutoSubmitLoginWorkflow() { + clearAutoSubmitLoginTimeout(); + updateIsFieldCurrentlyFilling(false); + } + + /** + * Handles the extension message used to trigger the auto-submit login action. + * + * @param command - The command to execute + * @param fillScript - The autofill script to use + * @param pageDetailsUrl - The URL of the page details + */ + async function handleExtensionMessage({ + command, + fillScript, + pageDetailsUrl, + }: { + command: string; + fillScript: AutofillScript; + pageDetailsUrl: string; + }) { + if ( + command !== "triggerAutoSubmitLogin" || + (globalContext.document.defaultView || globalContext).location.href !== pageDetailsUrl + ) { + return; + } + + await triggerAutoSubmitLogin(fillScript); + } + + /** + * Fills the fields set within the autofill script and triggers the auto-submit action. Will + * also set up a subsequent auto-submit action to continue the workflow on any multistep + * login forms. + * + * @param fillScript - The autofill script to use + */ + async function triggerAutoSubmitLogin(fillScript: AutofillScript) { + if (!fillScript?.autosubmit?.length) { + endUpAutoSubmitLoginWorkflow(); + throw new Error("Unable to auto-submit form, no autosubmit reference found."); + } + + updateIsFieldCurrentlyFilling(true); + await insertAutofillContentService.fillForm(fillScript); + setAutoSubmitLoginTimeout(400); + triggerAutoSubmitOnForm(fillScript); + } + + /** + * Triggers the auto-submit action on the form element. Will attempt to click an existing + * submit button, and if none are found, will attempt to submit the form directly. Note + * only the first matching field will be used to trigger the submit action. We will not + * attempt to trigger the submit action on multiple forms that might exist on a page. + * + * @param fillScript - The autofill script to use + */ + function triggerAutoSubmitOnForm(fillScript: AutofillScript) { + const formOpid = fillScript.autosubmit[0]; + + if (formOpid === null) { + triggerAutoSubmitOnFormlessFields(fillScript); + return; + } + + const formElement = getAutofillFormElementByOpid(formOpid); + if (!formElement) { + triggerAutoSubmitOnFormlessFields(fillScript); + return; + } + + if (submitElementFoundAndClicked(formElement)) { + return; + } + + if (formElement.requestSubmit) { + formElement.requestSubmit(); + return; + } + + formElement.submit(); + } + + /** + * Triggers the auto-submit action on formless fields. This is done by iterating up the DOM + * tree, and attempting to find a submit button or form element to trigger the submit action. + * + * @param fillScript - The autofill script to use + */ + function triggerAutoSubmitOnFormlessFields(fillScript: AutofillScript) { + let currentElement = collectAutofillContentService.getAutofillFieldElementByOpid( + fillScript.script[fillScript.script.length - 1][1], + ); + + const lastFieldIsPasswordInput = + elementIsInputElement(currentElement) && currentElement.type === "password"; + + while (currentElement && currentElement.tagName !== "HTML") { + if (submitElementFoundAndClicked(currentElement, lastFieldIsPasswordInput)) { + return; + } + + if (!currentElement.parentElement && currentElement.getRootNode() instanceof ShadowRoot) { + currentElement = (currentElement.getRootNode() as ShadowRoot).host as any; + continue; + } + + currentElement = currentElement.parentElement; + } + + if (!currentElement || currentElement.tagName === "HTML") { + endUpAutoSubmitLoginWorkflow(); + throw new Error("Unable to auto-submit form, no submit button or form element found."); + } + } + + /** + * Queries the element for an element of type="submit" or a button element with a keyword + * that matches a login action. If found, the element is clicked and the submit action is + * triggered. + * + * @param element - The element to query for a submit action + * @param lastFieldIsPasswordInput - Whether the last field is a password input + */ + function submitElementFoundAndClicked( + element: HTMLElement, + lastFieldIsPasswordInput = false, + ): boolean { + const genericSubmitElement = collectAutofillContentService.deepQueryElements( + element, + "[type='submit']", + ); + if (genericSubmitElement[0]) { + clickSubmitElement(genericSubmitElement[0], lastFieldIsPasswordInput); + return true; + } + + const buttons = collectAutofillContentService.deepQueryElements( + element, + "button", + ); + for (let i = 0; i < buttons.length; i++) { + if (isLoginButton(buttons[i])) { + clickSubmitElement(buttons[i], lastFieldIsPasswordInput); + return true; + } + } + + return false; + } + + /** + * Handles clicking the submit element and optionally triggering + * a completion action for multistep login forms. + * + * @param element - The element to click + * @param lastFieldIsPasswordInput - Whether the last field is a password input + */ + function clickSubmitElement(element: HTMLElement, lastFieldIsPasswordInput = false) { + if (lastFieldIsPasswordInput) { + triggerMultiStepAutoSubmitLoginComplete(); + } + + element.click(); + } + + /** + * Gathers attributes from the element and checks if any of the values match the login + * keywords. This is used to determine if the element is a login button. + * + * @param element - The element to check + */ + function isLoginButton(element: HTMLElement) { + const keywordValues = [ + element.textContent, + element.getAttribute("value"), + element.getAttribute("aria-label"), + element.getAttribute("aria-labelledby"), + element.getAttribute("aria-describedby"), + element.getAttribute("title"), + element.getAttribute("id"), + element.getAttribute("name"), + element.getAttribute("class"), + ] + .join(",") + .toLowerCase(); + + return loginKeywords.some((keyword) => keywordValues.includes(keyword)); + } + + /** + * Retrieves a form element by its opid attribute. + * + * @param opid - The opid to search for + */ + function getAutofillFormElementByOpid(opid: string): HTMLFormElement | null { + const cachedFormElements = Array.from( + collectAutofillContentService.autofillFormElements.keys(), + ); + const formElements = cachedFormElements?.length + ? cachedFormElements + : getAutofillFormElements(); + + return formElements.find((formElement) => formElement.opid === opid) || null; + } + + /** + * Gets all form elements on the page. + */ + function getAutofillFormElements(): HTMLFormElement[] { + const formElements: HTMLFormElement[] = []; + collectAutofillContentService.queryAllTreeWalkerNodes( + globalContext.document.documentElement, + (node: Node) => { + if (nodeIsFormElement(node)) { + formElements.push(node); + return true; + } + + return false; + }, + ); + + return formElements; + } + + /** + * Sets a timeout to trigger the auto-submit login workflow. + * + * @param delay - The delay to wait before triggering the workflow + */ + function setAutoSubmitLoginTimeout(delay: number) { + clearAutoSubmitLoginTimeout(); + autoSubmitLoginTimeout = globalContext.setTimeout(() => startAutoSubmitLoginWorkflow(), delay); + } + + /** + * Clears the auto-submit login timeout. + */ + function clearAutoSubmitLoginTimeout() { + if (autoSubmitLoginTimeout) { + globalContext.clearInterval(autoSubmitLoginTimeout); + } + } + + /** + * Triggers a completion action for multistep login forms. + */ + function triggerMultiStepAutoSubmitLoginComplete() { + endUpAutoSubmitLoginWorkflow(); + void sendExtensionMessage("multiStepAutoSubmitLoginComplete"); + } + + /** + * Updates the state of whether a field is currently being filled. This ensures that + * the inline menu is not displayed when a field is being filled. + * + * @param isFieldCurrentlyFilling - Whether a field is currently being filled + */ + function updateIsFieldCurrentlyFilling(isFieldCurrentlyFilling: boolean) { + void sendExtensionMessage("updateIsFieldCurrentlyFilling", { isFieldCurrentlyFilling }); + } +})(globalThis); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 70f815d2234..e44956e1849 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -3,7 +3,7 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillPageDetails from "../models/autofill-page-details"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; -import CollectAutofillContentService from "../services/collect-autofill-content.service"; +import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; import { sendExtensionMessage } from "../utils"; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts index 3e36fa43bbd..211e3bf9251 100644 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -1,6 +1,6 @@ import { AutofillInit } from "../../content/abstractions/autofill-init"; import AutofillPageDetails from "../../models/autofill-page-details"; -import CollectAutofillContentService from "../../services/collect-autofill-content.service"; +import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; import DomElementVisibilityService from "../../services/dom-element-visibility.service"; import InsertAutofillContentService from "../../services/insert-autofill-content.service"; import { sendExtensionMessage } from "../../utils"; diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts index 5f91e6c0813..4631b78ddb1 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts @@ -1,6 +1,4 @@ -import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; - -import { WebauthnUtils } from "../../../vault/fido2/webauthn-utils"; +import { WebauthnUtils } from "../utils/webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; @@ -126,13 +124,47 @@ import { Messenger } from "./messaging/messenger"; return await browserCredentials.get(options); } + const abortSignal = options?.signal || new AbortController().signal; const fallbackSupported = browserNativeWebauthnSupport; - try { - if (options?.mediation && options.mediation !== "optional") { - throw new FallbackRequestedError(); - } + if (options?.mediation && options.mediation === "conditional") { + const internalAbortControllers = [new AbortController(), new AbortController()]; + const bitwardenResponse = async (internalAbortController: AbortController) => { + try { + const response = await messenger.request( + { + type: MessageType.CredentialGetRequest, + data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), + }, + internalAbortController.signal, + ); + if (response.type !== MessageType.CredentialGetResponse) { + throw new Error("Something went wrong."); + } + return WebauthnUtils.mapCredentialAssertResult(response.result); + } catch { + // Ignoring error + } + }; + const browserResponse = (internalAbortController: AbortController) => + browserCredentials.get({ ...options, signal: internalAbortController.signal }); + const abortListener = () => { + internalAbortControllers.forEach((controller) => controller.abort()); + }; + abortSignal.addEventListener("abort", abortListener); + + const response = await Promise.race([ + bitwardenResponse(internalAbortControllers[0]), + browserResponse(internalAbortControllers[1]), + ]); + abortSignal.removeEventListener("abort", abortListener); + internalAbortControllers.forEach((controller) => controller.abort()); + + return response; + } + + try { const response = await messenger.request( { type: MessageType.CredentialGetRequest, diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts index 292d0e01182..31e8c941e86 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts @@ -5,7 +5,7 @@ import { createCredentialRequestOptionsMock, setupMockedWebAuthnSupport, } from "../../../autofill/spec/fido2-testing-utils"; -import { WebauthnUtils } from "../../../vault/fido2/webauthn-utils"; +import { WebauthnUtils } from "../utils/webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; @@ -41,7 +41,7 @@ jest.mock("./messaging/messenger", () => { }, }; }); -jest.mock("../../../vault/fido2/webauthn-utils"); +jest.mock("../utils/webauthn-utils"); describe("Fido2 page script with native WebAuthn support", () => { (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( @@ -128,6 +128,17 @@ describe("Fido2 page script with native WebAuthn support", () => { mockCredentialAssertResult, ); }); + + it("initiates a conditional mediated webauth request", async () => { + mockCredentialRequestOptions.mediation = "conditional"; + mockCredentialRequestOptions.signal = new AbortController().signal; + + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); }); describe("destroy", () => { diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts index a1e7006b045..e354453ca59 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts @@ -4,7 +4,7 @@ import { createCredentialCreationOptionsMock, createCredentialRequestOptionsMock, } from "../../../autofill/spec/fido2-testing-utils"; -import { WebauthnUtils } from "../../../vault/fido2/webauthn-utils"; +import { WebauthnUtils } from "../utils/webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; @@ -39,7 +39,7 @@ jest.mock("./messaging/messenger", () => { }, }; }); -jest.mock("../../../vault/fido2/webauthn-utils"); +jest.mock("../utils/webauthn-utils"); describe("Fido2 page script without native WebAuthn support", () => { (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( diff --git a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts similarity index 98% rename from apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts rename to apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index c618c3dd148..f373494d52d 100644 --- a/apps/browser/src/vault/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -25,8 +25,8 @@ import { } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { closeFido2Popout, openFido2Popout } from "../popup/utils/vault-popout-window"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { closeFido2Popout, openFido2Popout } from "../../../vault/popup/utils/vault-popout-window"; const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage"; diff --git a/apps/browser/src/vault/fido2/webauthn-utils.ts b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts similarity index 98% rename from apps/browser/src/vault/fido2/webauthn-utils.ts rename to apps/browser/src/autofill/fido2/utils/webauthn-utils.ts index df8e5a8fb20..71ed5dc000e 100644 --- a/apps/browser/src/vault/fido2/webauthn-utils.ts +++ b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts @@ -7,7 +7,7 @@ import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-util import { InsecureAssertCredentialParams, InsecureCreateCredentialParams, -} from "../../autofill/fido2/content/messaging/message"; +} from "../content/messaging/message"; export class WebauthnUtils { static mapCredentialCreationOptions( @@ -111,6 +111,7 @@ export class WebauthnUtils { rpId: keyOptions.rpId, userVerification: keyOptions.userVerification, timeout: keyOptions.timeout, + mediation: options.mediation, fallbackSupported, }; } diff --git a/apps/browser/src/autofill/models/autofill-script.ts b/apps/browser/src/autofill/models/autofill-script.ts index 7ab96eb2856..b9d6f1a1495 100644 --- a/apps/browser/src/autofill/models/autofill-script.ts +++ b/apps/browser/src/autofill/models/autofill-script.ts @@ -17,7 +17,7 @@ export default class AutofillScript { script: FillScript[] = []; properties: AutofillScriptProperties = {}; metadata: any = {}; // Unused, not written or read - autosubmit: any = null; // Appears to be unused, read but not written + autosubmit: string[]; // Appears to be unused, read but not written savedUrls: string[]; untrustedIframe: boolean; itemType: string; // Appears to be unused, read but not written diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts index 090fb7887c9..ea584165b4d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts @@ -18,6 +18,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & ciphers?: InlineMenuCipherData[]; filledByCipherType?: CipherType; showInlineMenuAccountCreation?: boolean; + showPasskeysLabels?: boolean; portKey: string; }; diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index a8a4d5c4a78..93d757fc51e 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -478,7 +478,6 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f class="cipher-container" > + + + +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
  • +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • +
  • +
    + + +
    +
  • + + +`; + exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
    { fillCipherButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "fillAutofillInlineMenuCipher", inlineMenuCipherId: "1", portKey }, + { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "1", + usePasskey: false, + portKey, + }, "*", ); }); @@ -504,6 +510,178 @@ describe("AutofillInlineMenuList", () => { }); }); }); + + describe("creating a list of passkeys", () => { + let passkeyCipher1: InlineMenuCipherData; + let passkeyCipher2: InlineMenuCipherData; + let passkeyCipher3: InlineMenuCipherData; + let loginCipher1: InlineMenuCipherData; + let loginCipher2: InlineMenuCipherData; + let loginCipher3: InlineMenuCipherData; + let loginCipher4: InlineMenuCipherData; + const borderClass = "inline-menu-list-heading--bordered"; + + beforeEach(() => { + passkeyCipher1 = createAutofillOverlayCipherDataMock(1, { + name: "https://example.com", + login: { + username: "username1", + passkey: { + rpName: "https://example.com", + userName: "username1", + }, + }, + }); + passkeyCipher2 = createAutofillOverlayCipherDataMock(2, { + name: "https://example.com", + login: { + username: "", + passkey: { + rpName: "https://example.com", + userName: "username2", + }, + }, + }); + passkeyCipher3 = createAutofillOverlayCipherDataMock(3, { + login: { + username: "username3", + passkey: { + rpName: "https://example.com", + userName: "username3", + }, + }, + }); + loginCipher1 = createAutofillOverlayCipherDataMock(1, { + login: { + username: "username1", + passkey: null, + }, + }); + loginCipher2 = createAutofillOverlayCipherDataMock(2, { + login: { + username: "username2", + passkey: null, + }, + }); + loginCipher3 = createAutofillOverlayCipherDataMock(3, { + login: { + username: "username3", + passkey: null, + }, + }); + loginCipher4 = createAutofillOverlayCipherDataMock(4, { + login: { + username: "username4", + passkey: null, + }, + }); + postWindowMessage( + createInitAutofillInlineMenuListMessageMock({ + ciphers: [ + passkeyCipher1, + passkeyCipher2, + passkeyCipher3, + loginCipher1, + loginCipher2, + loginCipher3, + loginCipher4, + ], + showPasskeysLabels: true, + portKey, + }), + ); + }); + + it("renders the passkeys list item views", () => { + expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot(); + }); + + describe("passkeys headings on scroll", () => { + it("adds a border class to the passkeys and login headings when the user scrolls the cipher list container", () => { + autofillInlineMenuList["ciphersList"].scrollTop = 300; + + autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll")); + + expect( + autofillInlineMenuList["passkeysHeadingElement"].classList.contains(borderClass), + ).toBe(true); + expect(autofillInlineMenuList["passkeysHeadingElement"].style.position).toBe( + "relative", + ); + expect( + autofillInlineMenuList["loginHeadingElement"].classList.contains(borderClass), + ).toBe(true); + }); + + it("removes the border class from the passkeys and login headings when the user scrolls the cipher list container to the top", () => { + jest.useFakeTimers(); + autofillInlineMenuList["ciphersList"].scrollTop = 300; + + autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll")); + jest.advanceTimersByTime(75); + + autofillInlineMenuList["ciphersList"].scrollTop = -1; + autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll")); + + expect( + autofillInlineMenuList["passkeysHeadingElement"].classList.contains(borderClass), + ).toBe(false); + expect(autofillInlineMenuList["passkeysHeadingElement"].style.position).toBe(""); + expect( + autofillInlineMenuList["loginHeadingElement"].classList.contains(borderClass), + ).toBe(false); + }); + + it("loads each page of ciphers until the list of updated ciphers is exhausted", () => { + jest.useFakeTimers(); + autofillInlineMenuList["ciphersList"].scrollTop = 10; + jest.spyOn(autofillInlineMenuList as any, "loadPageOfCiphers"); + + autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll")); + jest.advanceTimersByTime(1000); + autofillInlineMenuList["ciphersList"].dispatchEvent(new Event("scroll")); + jest.runAllTimers(); + + expect(autofillInlineMenuList["loadPageOfCiphers"]).toHaveBeenCalledTimes(1); + }); + }); + + it("skips the logins heading when the user presses ArrowDown to focus the next list item", () => { + const cipherContainerElements = + autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li"); + const viewCipherButton = cipherContainerElements[3].querySelector(".view-cipher-button"); + const fillCipherButton = cipherContainerElements[5].querySelector(".fill-cipher-button"); + jest.spyOn(fillCipherButton as HTMLElement, "focus"); + + viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" })); + + expect((fillCipherButton as HTMLElement).focus).toBeCalled(); + }); + + it("skips the passkeys heading when the user presses ArrowDown to focus the first list item", () => { + const cipherContainerElements = + autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li"); + const viewCipherButton = cipherContainerElements[7].querySelector(".view-cipher-button"); + const fillCipherButton = cipherContainerElements[1].querySelector(".fill-cipher-button"); + jest.spyOn(fillCipherButton as HTMLElement, "focus"); + + viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowDown" })); + + expect((fillCipherButton as HTMLElement).focus).toBeCalled(); + }); + + it("skips the logins heading when the user presses ArrowUp to focus the previous list item", () => { + const cipherContainerElements = + autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll("li"); + const viewCipherButton = cipherContainerElements[5].querySelector(".view-cipher-button"); + const fillCipherButton = cipherContainerElements[3].querySelector(".fill-cipher-button"); + jest.spyOn(fillCipherButton as HTMLElement, "focus"); + + viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowUp" })); + + expect((fillCipherButton as HTMLElement).focus).toBeCalled(); + }); + }); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index 8bccf9aae47..6ec0bc83991 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -1,12 +1,18 @@ import "@webcomponents/custom-elements"; import "lit/polyfill-support.js"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { EVENTS } from "@bitwarden/common/autofill/constants"; +import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants"; import { CipherType } from "@bitwarden/common/vault/enums"; import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background"; -import { buildSvgDomElement } from "../../../../utils"; -import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../../utils/svg-icons"; +import { buildSvgDomElement, throttle } from "../../../../utils"; +import { + globeIcon, + lockIcon, + plusIcon, + viewCipherIcon, + passkeyIcon, +} from "../../../../utils/svg-icons"; import { AutofillInlineMenuListWindowMessageHandlers, InitAutofillInlineMenuListMessage, @@ -24,8 +30,16 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { private currentCipherIndex = 0; private filledByCipherType: CipherType; private showInlineMenuAccountCreation: boolean; - private readonly showCiphersPerPage = 6; + private showPasskeysLabels: boolean; private newItemButtonElement: HTMLButtonElement; + private passkeysHeadingElement: HTMLLIElement; + private loginHeadingElement: HTMLLIElement; + private lastPasskeysListItem: HTMLLIElement; + private passkeysHeadingHeight: number; + private lastPasskeysListItemHeight: number; + private ciphersListHeight: number; + private readonly showCiphersPerPage = 6; + private readonly headingBorderClass = "inline-menu-list-heading--bordered"; private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers = { initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message), @@ -53,6 +67,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param portKey - Background generated key that allows the port to communicate with the background. * @param filledByCipherType - The type of cipher that fills the current field. * @param showInlineMenuAccountCreation - Whether identity ciphers are shown on login fields. + * @param showPasskeysLabels - Whether passkeys labels are shown in the inline menu list. */ private async initAutofillInlineMenuList({ translations, @@ -63,6 +78,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { portKey, filledByCipherType, showInlineMenuAccountCreation, + showPasskeysLabels, }: InitAutofillInlineMenuListMessage) { const linkElement = await this.initAutofillInlineMenuPage( "list", @@ -72,6 +88,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { ); this.filledByCipherType = filledByCipherType; + this.showPasskeysLabels = showPasskeysLabels; const themeClass = `theme_${theme}`; globalThis.document.documentElement.classList.add(themeClass); @@ -155,9 +172,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.ciphersList = globalThis.document.createElement("ul"); this.ciphersList.classList.add("inline-menu-list-actions"); this.ciphersList.setAttribute("role", "list"); - this.ciphersList.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent, { - passive: true, - }); + this.setupCipherListScrollListeners(); this.loadPageOfCiphers(); @@ -288,8 +303,35 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.currentCipherIndex++; } - if (this.currentCipherIndex >= this.ciphers.length) { - this.ciphersList.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent); + if (!this.showPasskeysLabels && this.allCiphersLoaded()) { + this.ciphersList.removeEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll); + } + } + + /** + * Validates whether the list of ciphers has been fully loaded. + */ + private allCiphersLoaded() { + return this.currentCipherIndex >= this.ciphers.length; + } + + /** + * Sets up the scroll listeners for the ciphers list. These are used to trigger an update of + * the list of ciphers when the user scrolls to the bottom of the list. Also sets up the + * scroll listeners that reposition the passkeys and login headings when the user scrolls. + */ + private setupCipherListScrollListeners() { + const options = { passive: true }; + this.ciphersList.addEventListener(EVENTS.SCROLL, this.updateCiphersListOnScroll, options); + if (this.showPasskeysLabels) { + this.ciphersList.addEventListener( + EVENTS.SCROLL, + this.useEventHandlersMemo( + throttle(() => this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop), 50), + UPDATE_PASSKEYS_HEADINGS_ON_SCROLL, + ), + options, + ); } } @@ -297,7 +339,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles updating the list of ciphers when the * user scrolls to the bottom of the list. */ - private handleCiphersListScrollEvent = () => { + private updateCiphersListOnScroll = () => { if (this.cipherListScrollIsDebounced) { return; } @@ -318,22 +360,109 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { */ private handleDebouncedScrollEvent = () => { this.cipherListScrollIsDebounced = false; + const cipherListScrollTop = this.ciphersList.scrollTop; + + this.updatePasskeysHeadingsOnScroll(cipherListScrollTop); + + if (this.allCiphersLoaded()) { + return; + } + + if (!this.ciphersListHeight) { + this.ciphersListHeight = this.ciphersList.offsetHeight; + } const scrollPercentage = - (this.ciphersList.scrollTop / - (this.ciphersList.scrollHeight - this.ciphersList.offsetHeight)) * - 100; + (cipherListScrollTop / (this.ciphersList.scrollHeight - this.ciphersListHeight)) * 100; if (scrollPercentage >= 80) { this.loadPageOfCiphers(); } }; + /** + * Updates the passkeys and login headings when the user scrolls the ciphers list. + * + * @param cipherListScrollTop - The current scroll top position of the ciphers list. + */ + private updatePasskeysHeadingsOnScroll = (cipherListScrollTop: number) => { + if (!this.showPasskeysLabels) { + return; + } + + if (this.passkeysHeadingElement) { + this.togglePasskeysHeadingAnchored(cipherListScrollTop); + this.togglePasskeysHeadingBorder(cipherListScrollTop); + } + + if (this.loginHeadingElement) { + this.toggleLoginHeadingBorder(cipherListScrollTop); + } + }; + + /** + * Anchors the passkeys heading to the top of the last passkey item when the user scrolls. + * + * @param cipherListScrollTop - The current scroll top position of the ciphers list. + */ + private togglePasskeysHeadingAnchored(cipherListScrollTop: number) { + if (!this.passkeysHeadingHeight) { + this.passkeysHeadingHeight = this.passkeysHeadingElement.offsetHeight; + } + + const passkeysHeadingOffset = this.lastPasskeysListItem.offsetTop - this.passkeysHeadingHeight; + if (cipherListScrollTop >= passkeysHeadingOffset) { + this.passkeysHeadingElement.style.position = "relative"; + this.passkeysHeadingElement.style.top = `${passkeysHeadingOffset}px`; + + return; + } + + this.passkeysHeadingElement.setAttribute("style", ""); + } + + /** + * Toggles a border on the passkeys heading on scroll, adding it when the user has + * scrolled at all and removing it once the user scrolls back to the top. + * + * @param cipherListScrollTop - The current scroll top position of the ciphers list. + */ + private togglePasskeysHeadingBorder(cipherListScrollTop: number) { + if (cipherListScrollTop < 1) { + this.passkeysHeadingElement.classList.remove(this.headingBorderClass); + return; + } + + this.passkeysHeadingElement.classList.add(this.headingBorderClass); + } + + /** + * Toggles a border on the login heading on scroll, adding it when the user has + * scrolled past the last passkey item and removing it once the user scrolls back up. + * + * @param cipherListScrollTop - The current scroll top position of the ciphers list. + */ + private toggleLoginHeadingBorder(cipherListScrollTop: number) { + if (!this.lastPasskeysListItemHeight) { + this.lastPasskeysListItemHeight = this.lastPasskeysListItem.offsetHeight; + } + + const lastPasskeyOffset = this.lastPasskeysListItem.offsetTop + this.lastPasskeysListItemHeight; + if (cipherListScrollTop < lastPasskeyOffset) { + this.loginHeadingElement.classList.remove(this.headingBorderClass); + return; + } + + this.loginHeadingElement.classList.add(this.headingBorderClass); + } + /** * Builds the list item for a given cipher. * * @param cipher - The cipher to build the list item for. */ private buildInlineMenuListActionsItem(cipher: InlineMenuCipherData) { + this.buildPasskeysHeadingElements(cipher); + const fillCipherElement = this.buildFillCipherElement(cipher); const viewCipherElement = this.buildViewCipherElement(cipher); @@ -346,9 +475,43 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { inlineMenuListActionsItem.classList.add("inline-menu-list-actions-item"); inlineMenuListActionsItem.appendChild(cipherContainerElement); + if (this.showPasskeysLabels && cipher.login?.passkey) { + this.lastPasskeysListItem = inlineMenuListActionsItem; + } + return inlineMenuListActionsItem; } + /** + * Builds the passkeys and login headings for the list of cipher items. + * + * @param cipher - The cipher that will follow the heading. + */ + private buildPasskeysHeadingElements(cipher: InlineMenuCipherData) { + if (!this.showPasskeysLabels || (this.passkeysHeadingElement && this.loginHeadingElement)) { + return; + } + + const passkeyData = cipher.login?.passkey; + if (!this.passkeysHeadingElement && passkeyData) { + this.passkeysHeadingElement = globalThis.document.createElement("li"); + this.passkeysHeadingElement.classList.add("inline-menu-list-heading"); + this.passkeysHeadingElement.textContent = this.getTranslation("passkeys"); + this.ciphersList.appendChild(this.passkeysHeadingElement); + + return; + } + + if (!this.passkeysHeadingElement || this.loginHeadingElement || passkeyData) { + return; + } + + this.loginHeadingElement = globalThis.document.createElement("li"); + this.loginHeadingElement.classList.add("inline-menu-list-heading"); + this.loginHeadingElement.textContent = this.getTranslation("passwords"); + this.ciphersList.appendChild(this.loginHeadingElement); + } + /** * Builds the fill cipher button for a given cipher. * Wraps the cipher icon and details. @@ -364,8 +527,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { fillCipherElement.classList.add("fill-cipher-button", "inline-menu-list-action"); fillCipherElement.setAttribute( "aria-label", - `${this.getTranslation("fillCredentialsFor")} ${cipher.name}`, + `${ + cipher.login?.passkey + ? this.getTranslation("logInWithPasskey") + : this.getTranslation("fillCredentialsFor") + } ${cipher.name}`, ); + this.addFillCipherElementAriaDescription(fillCipherElement, cipher); fillCipherElement.append(cipherIcon, cipherDetailsElement); fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher)); @@ -385,10 +553,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { cipher: InlineMenuCipherData, ) { if (cipher.login) { - fillCipherElement.setAttribute( - "aria-description", - `${this.getTranslation("username")}: ${cipher.login.username}`, - ); + const passkeyUserName = cipher.login.passkey?.userName || ""; + const username = cipher.login.username || passkeyUserName; + if (username) { + fillCipherElement.setAttribute( + "aria-description", + `${this.getTranslation("username")}: ${username}`, + ); + } return; } @@ -419,13 +591,15 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipher - The cipher to fill. */ private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => { + const usePasskey = !!cipher.login?.passkey; return this.useEventHandlersMemo( () => this.postMessageToParent({ command: "fillAutofillInlineMenuCipher", inlineMenuCipherId: cipher.id, + usePasskey, }), - `${cipher.id}-fill-cipher-button-click-handler`, + `${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`, ); }; @@ -599,14 +773,20 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param cipher - The cipher to build the details for. */ private buildCipherDetailsElement(cipher: InlineMenuCipherData) { - const cipherNameElement = this.buildCipherNameElement(cipher); - const cipherSubtitleElement = this.buildCipherSubtitleElement(cipher); - const cipherDetailsElement = globalThis.document.createElement("span"); cipherDetailsElement.classList.add("cipher-details"); + + const cipherNameElement = this.buildCipherNameElement(cipher); if (cipherNameElement) { cipherDetailsElement.appendChild(cipherNameElement); } + + if (cipher.login?.passkey) { + return this.buildPasskeysCipherDetailsElement(cipher, cipherDetailsElement); + } + + const subTitleText = this.getSubTitleText(cipher); + const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText); if (cipherSubtitleElement) { cipherDetailsElement.appendChild(cipherSubtitleElement); } @@ -635,10 +815,9 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { /** * Builds the subtitle element for a given cipher. * - * @param cipher - The cipher to build the username login element for. + * @param subTitleText - The subtitle text to display. */ - private buildCipherSubtitleElement(cipher: InlineMenuCipherData): HTMLSpanElement | null { - const subTitleText = this.getSubTitleText(cipher); + private buildCipherSubtitleElement(subTitleText: string): HTMLSpanElement | null { if (!subTitleText) { return null; } @@ -651,6 +830,52 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { return cipherSubtitleElement; } + /** + * Builds the passkeys details for a given cipher. Includes the passkey name and username. + * + * @param cipher - The cipher to build the passkey details for. + * @param cipherDetailsElement - The cipher details element to append the passkey details to. + */ + private buildPasskeysCipherDetailsElement( + cipher: InlineMenuCipherData, + cipherDetailsElement: HTMLSpanElement, + ): HTMLSpanElement { + let rpNameSubtitle: HTMLSpanElement; + + if (cipher.name !== cipher.login.passkey.rpName) { + rpNameSubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.rpName); + if (rpNameSubtitle) { + rpNameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); + rpNameSubtitle.classList.add("cipher-subtitle--passkey"); + cipherDetailsElement.appendChild(rpNameSubtitle); + } + } + + if (cipher.login.username) { + const usernameSubtitle = this.buildCipherSubtitleElement(cipher.login.username); + if (usernameSubtitle) { + if (!rpNameSubtitle) { + usernameSubtitle.prepend(buildSvgDomElement(passkeyIcon)); + usernameSubtitle.classList.add("cipher-subtitle--passkey"); + } + cipherDetailsElement.appendChild(usernameSubtitle); + } + + return cipherDetailsElement; + } + + const passkeySubtitle = this.buildCipherSubtitleElement(cipher.login.passkey.userName); + if (passkeySubtitle) { + if (!rpNameSubtitle) { + passkeySubtitle.prepend(buildSvgDomElement(passkeyIcon)); + passkeySubtitle.classList.add("cipher-subtitle--passkey"); + } + cipherDetailsElement.appendChild(passkeySubtitle); + } + + return cipherDetailsElement; + } + /** * Gets the subtitle text for a given cipher. * @@ -779,7 +1004,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param currentListItem - The current list item. */ private focusNextListItem(currentListItem: HTMLElement) { - const nextListItem = currentListItem.nextSibling as HTMLElement; + let nextListItem = currentListItem.nextSibling as HTMLElement; + if (this.listItemIsHeadingElement(nextListItem)) { + nextListItem = nextListItem.nextSibling as HTMLElement; + } + const nextSibling = nextListItem?.querySelector(".inline-menu-list-action") as HTMLElement; if (nextSibling) { nextSibling.focus(); @@ -791,7 +1020,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { return; } - const firstListItem = currentListItem.parentElement?.firstChild as HTMLElement; + let firstListItem = currentListItem.parentElement?.firstChild as HTMLElement; + if (this.listItemIsHeadingElement(firstListItem)) { + firstListItem = firstListItem.nextSibling as HTMLElement; + } + const firstSibling = firstListItem?.querySelector(".inline-menu-list-action") as HTMLElement; firstSibling?.focus(); } @@ -803,7 +1036,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * @param currentListItem - The current list item. */ private focusPreviousListItem(currentListItem: HTMLElement) { - const previousListItem = currentListItem.previousSibling as HTMLElement; + let previousListItem = currentListItem.previousSibling as HTMLElement; + if (this.listItemIsHeadingElement(previousListItem)) { + previousListItem = previousListItem.previousSibling as HTMLElement; + } + const previousSibling = previousListItem?.querySelector( ".inline-menu-list-action", ) as HTMLElement; @@ -856,4 +1093,13 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { private isFilledByIdentityCipher = () => { return this.filledByCipherType === CipherType.Identity; }; + + /** + * Identifies if the passed list item is a heading element. + * + * @param listItem - The list item to check. + */ + private listItemIsHeadingElement = (listItem: HTMLElement) => { + return listItem === this.passkeysHeadingElement || listItem === this.loginHeadingElement; + }; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss index a63a4bd91ca..fe38ce9933f 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss @@ -166,6 +166,35 @@ body { } } +.inline-menu-list-heading { + position: sticky; + top: 0; + z-index: 1; + font-family: $font-family-sans-serif; + font-weight: 600; + font-size: 1rem; + line-height: 1.3; + letter-spacing: 0.025rem; + width: 100%; + padding: 0.6rem 0.8rem; + will-change: transform; + border-bottom: 0.1rem solid; + + @include themify($themes) { + color: themed("textColor"); + background-color: themed("backgroundColor"); + border-bottom-color: themed("backgroundColor"); + } + + &--bordered { + transition: border-bottom-color 0.15s ease; + + @include themify($themes) { + border-bottom-color: themed("borderColor"); + } + } +} + .inline-menu-list-container--with-new-item-button { .inline-menu-list-actions { max-height: 13.8rem; @@ -340,5 +369,28 @@ body { @include themify($themes) { color: themed("mutedTextColor"); } + + &--passkey { + display: flex; + align-content: center; + align-items: center; + justify-content: flex-start; + + svg { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.2rem; + + @include themify($themes) { + fill: themed("mutedTextColor") !important; + } + + path { + @include themify($themes) { + fill: themed("mutedTextColor") !important; + } + } + } + } } } diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index b97c4102fed..d9a7c7c9cbc 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -9,8 +9,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { BrowserFido2UserInterfaceSession } from "../../../vault/fido2/browser-fido2-user-interface.service"; import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; +import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; @Component({ selector: "app-fido2-use-browser-link", diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index d720a5240f7..43e8ce6809c 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -30,12 +30,12 @@ import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; +import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, -} from "../../../vault/fido2/browser-fido2-user-interface.service"; -import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; -import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; +} from "../../fido2/services/browser-fido2-user-interface.service"; interface ViewData { message: BrowserFido2Message; diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 0933bc54217..77f96612c85 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -145,8 +145,12 @@ type="checkbox" (change)="updateAutofillOnPageLoad()" [(ngModel)]="enableAutofillOnPageLoad" + [disabled]="autofillOnPageLoadFromPolicy$ | async" /> {{ "enableAutoFillOnPageLoad" | i18n }} + {{ + "enterprisePolicyRequirementsApplied" | i18n + }} {{ "defaultAutoFillOnPageLoad" | i18n }} diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 617041a2b12..dc6a4a0880e 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -87,6 +87,9 @@ export class AutofillComponent implements OnInit { DisablePasswordManagerUris.Unknown; protected browserShortcutsURI: BrowserShortcutsUri = BrowserShortcutsUris.Unknown; protected browserClientIsUnknown: boolean; + protected autofillOnPageLoadFromPolicy$ = + this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$; + enableAutofillOnPageLoad = false; enableInlineMenu = false; enableInlineMenuOnIconSelect = false; diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.html b/apps/browser/src/autofill/popup/settings/notifications.component.html index 9032e742da1..26036acdd0a 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.html +++ b/apps/browser/src/autofill/popup/settings/notifications.component.html @@ -11,39 +11,40 @@

    {{ "vaultSaveOptionsTitle" | i18n }}

    -
    + - -
    -
    + {{ "enableUsePasskeys" | i18n }} + + - -
    -
    + {{ + "enableAddLoginNotification" | i18n + }} + + - -
    + }} +
    diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 9bdb85a3f2a..378f9eaddf1 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -28,6 +28,7 @@ export interface AutoFillOptions { skipLastUsed?: boolean; allowUntrustedIframe?: boolean; allowTotpAutofill?: boolean; + autoSubmitLogin?: boolean; } export interface FormData { @@ -43,6 +44,7 @@ export interface GenerateFillScriptOptions { onlyVisibleFields: boolean; fillNewPassword: boolean; allowTotpAutofill: boolean; + autoSubmitLogin: boolean; cipher: CipherView; tabUrl: string; defaultUriMatch: UriMatchStrategySetting; @@ -75,11 +77,13 @@ export abstract class AutofillService { pageDetails: PageDetail[], tab: chrome.tabs.Tab, fromCommand: boolean, + autoSubmitLogin?: boolean, ) => Promise; doAutoFillActiveTab: ( pageDetails: PageDetail[], fromCommand: boolean, cipherType?: CipherType, ) => Promise; + setAutoFillOnPageLoadOrgPolicy: () => Promise; isPasswordRepromptRequired: (cipher: CipherView, tab: chrome.tabs.Tab) => Promise; } diff --git a/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts index d17d7842dcc..df9b7a5f2f4 100644 --- a/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/collect-autofill-content.service.ts @@ -15,6 +15,7 @@ type UpdateAutofillDataAttributeParams = { }; interface CollectAutofillContentService { + autofillFormElements: AutofillFormElements; getPageDetails(): Promise; getAutofillFieldElementByOpid(opid: string): HTMLElement | null; deepQueryElements( @@ -22,6 +23,11 @@ interface CollectAutofillContentService { selector: string, isObservingShadowRoot?: boolean, ): T[]; + queryAllTreeWalkerNodes( + rootNode: Node, + filterCallback: CallableFunction, + isObservingShadowRoot?: boolean, + ): Node[]; destroy(): void; } 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 064c76b657e..5b641e46c18 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -1428,7 +1428,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ !globalThis.isNaN(focusedFieldRectsTop) && focusedFieldRectsTop >= 0 && focusedFieldRectsTop < viewportHeight && - focusedFieldRectsBottom < viewportHeight + focusedFieldRectsBottom <= viewportHeight ); } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index aca82227284..90e49b15db9 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -696,6 +696,7 @@ describe("AutofillService", () => { onlyVisibleFields: autofillOptions.onlyVisibleFields || false, fillNewPassword: autofillOptions.fillNewPassword || false, allowTotpAutofill: autofillOptions.allowTotpAutofill || false, + autoSubmitLogin: autofillOptions.allowTotpAutofill || false, cipher: autofillOptions.cipher, tabUrl: autofillOptions.tab.url, defaultUriMatch: 0, @@ -709,7 +710,6 @@ describe("AutofillService", () => { { command: "fillForm", fillScript: { - autosubmit: null, metadata: {}, properties: { delay_between_operations: 20, @@ -1015,6 +1015,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1044,6 +1045,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1070,6 +1072,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1548,7 +1551,6 @@ describe("AutofillService", () => { expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ @@ -1587,7 +1589,6 @@ describe("AutofillService", () => { expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ @@ -1626,7 +1627,6 @@ describe("AutofillService", () => { expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index d9ae4e99237..632d6896d21 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -346,6 +346,7 @@ export default class AutofillService implements AutofillServiceInterface { onlyVisibleFields: options.onlyVisibleFields || false, fillNewPassword: options.fillNewPassword || false, allowTotpAutofill: options.allowTotpAutofill || false, + autoSubmitLogin: options.autoSubmitLogin || false, cipher: options.cipher, tabUrl: tab.url, defaultUriMatch: defaultUriMatch, @@ -379,7 +380,7 @@ export default class AutofillService implements AutofillServiceInterface { BrowserApi.tabSendMessage( tab, { - command: "fillForm", + command: options.autoSubmitLogin ? "triggerAutoSubmitLogin" : "fillForm", fillScript: fillScript, url: tab.url, pageDetailsUrl: pd.details.url, @@ -424,12 +425,14 @@ export default class AutofillService implements AutofillServiceInterface { * @param {PageDetail[]} pageDetails The data scraped from the page * @param {chrome.tabs.Tab} tab The tab to be autofilled * @param {boolean} fromCommand Whether the autofill is triggered by a keyboard shortcut (`true`) or autofill on page load (`false`) + * @param {boolean} autoSubmitLogin Whether the autofill is for an auto-submit login * @returns {Promise} The TOTP code of the successfully autofilled login, if any */ async doAutoFillOnTab( pageDetails: PageDetail[], tab: chrome.tabs.Tab, fromCommand: boolean, + autoSubmitLogin = false, ): Promise { let cipher: CipherView; if (fromCommand) { @@ -469,6 +472,7 @@ export default class AutofillService implements AutofillServiceInterface { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin, }); // Update last used index as autofill has succeeded @@ -570,6 +574,19 @@ export default class AutofillService implements AutofillServiceInterface { return totpCode; } + /** + * Activates the autofill on page load org policy. + */ + async setAutoFillOnPageLoadOrgPolicy(): Promise { + const autofillOnPageLoadOrgPolicy = await firstValueFrom( + this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, + ); + + if (autofillOnPageLoadOrgPolicy) { + await this.autofillSettingsService.setAutofillOnPageLoad(true); + } + } + /** * Gets the active tab from the current window. * Throws an error if no tab is found. @@ -820,6 +837,7 @@ export default class AutofillService implements AutofillServiceInterface { }); } + const formElementsSet = new Set(); usernames.forEach((u) => { // eslint-disable-next-line if (filledFields.hasOwnProperty(u.opid)) { @@ -828,6 +846,7 @@ export default class AutofillService implements AutofillServiceInterface { filledFields[u.opid] = u; AutofillService.fillByOpid(fillScript, u, login.username); + formElementsSet.add(u.form); }); passwords.forEach((p) => { @@ -838,8 +857,13 @@ export default class AutofillService implements AutofillServiceInterface { filledFields[p.opid] = p; AutofillService.fillByOpid(fillScript, p, login.password); + formElementsSet.add(p.form); }); + if (options.autoSubmitLogin && formElementsSet.size) { + fillScript.autosubmit = Array.from(formElementsSet); + } + if (options.allowTotpAutofill) { await Promise.all( totps.map(async (t) => { diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index f67c0e88aa0..40a370d70cd 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -13,7 +13,7 @@ import { import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; -import CollectAutofillContentService from "./collect-autofill-content.service"; +import { CollectAutofillContentService } from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; const mockLoginForm = ` @@ -155,7 +155,7 @@ describe("CollectAutofillContentService", () => { "data-stripe": null, }; collectAutofillContentService["domRecentlyMutated"] = false; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["autofillFieldElements"] = new Map([ @@ -243,7 +243,7 @@ describe("CollectAutofillContentService", () => { "data-stripe": null, }; collectAutofillContentService["domRecentlyMutated"] = false; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["autofillFieldElements"] = new Map([ @@ -527,7 +527,7 @@ describe("CollectAutofillContentService", () => { htmlID: formId, htmlMethod: formMethod, }; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, existingAutofillForm], ]); const formElements = Array.from(document.querySelectorAll("form")); @@ -2135,7 +2135,7 @@ describe("CollectAutofillContentService", () => { const removedNodes = document.querySelectorAll("form"); const autofillForm: AutofillForm = createAutofillFormMock({}); const autofillField: AutofillField = createAutofillFieldMock({}); - collectAutofillContentService["autofillFormElements"] = new Map([[form, autofillForm]]); + collectAutofillContentService["_autofillFormElements"] = new Map([[form, autofillForm]]); collectAutofillContentService["autofillFieldElements"] = new Map([ [usernameInput, autofillField], ]); @@ -2158,7 +2158,7 @@ describe("CollectAutofillContentService", () => { ]); await waitForIdleCallback(); - expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); + expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0); expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0); }); @@ -2280,13 +2280,13 @@ describe("CollectAutofillContentService", () => { htmlAction: "https://example.com", htmlMethod: "POST", }; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["deleteCachedAutofillElement"](formElement); - expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); + expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0); }); it("removes the autofill field element form the map of elements", () => { @@ -2332,7 +2332,7 @@ describe("CollectAutofillContentService", () => { expect(collectAutofillContentService["domRecentlyMutated"]).toEqual(true); expect(collectAutofillContentService["noFieldsFound"]).toEqual(false); expect(collectAutofillContentService["updateAutofillElementsAfterMutation"]).toBeCalled(); - expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); + expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0); expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0); }); }); @@ -2379,7 +2379,9 @@ describe("CollectAutofillContentService", () => { removedNodes: null, target: targetNode, }; - collectAutofillContentService["autofillFormElements"] = new Map([[targetNode, autofillForm]]); + collectAutofillContentService["_autofillFormElements"] = new Map([ + [targetNode, autofillForm], + ]); jest.spyOn(collectAutofillContentService as any, "updateAutofillFormElementData"); collectAutofillContentService["handleAutofillElementAttributeMutation"](mutationRecord); @@ -2451,14 +2453,14 @@ describe("CollectAutofillContentService", () => { const updatedAttributes = ["action", "name", "id", "method"]; beforeEach(() => { - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); }); updatedAttributes.forEach((attribute) => { it(`will update the ${attribute} value for the form element`, () => { - jest.spyOn(collectAutofillContentService["autofillFormElements"], "set"); + jest.spyOn(collectAutofillContentService["_autofillFormElements"], "set"); collectAutofillContentService["updateAutofillFormElementData"]( attribute, @@ -2466,7 +2468,7 @@ describe("CollectAutofillContentService", () => { autofillForm, ); - expect(collectAutofillContentService["autofillFormElements"].set).toBeCalledWith( + expect(collectAutofillContentService["_autofillFormElements"].set).toBeCalledWith( formElement, autofillForm, ); @@ -2474,7 +2476,7 @@ describe("CollectAutofillContentService", () => { }); it("will not update an attribute value if it is not present in the updateActions object", () => { - jest.spyOn(collectAutofillContentService["autofillFormElements"], "set"); + jest.spyOn(collectAutofillContentService["_autofillFormElements"], "set"); collectAutofillContentService["updateAutofillFormElementData"]( "aria-label", @@ -2482,7 +2484,7 @@ describe("CollectAutofillContentService", () => { autofillForm, ); - expect(collectAutofillContentService["autofillFormElements"].set).not.toBeCalled(); + expect(collectAutofillContentService["_autofillFormElements"].set).not.toBeCalled(); }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 8ba46fc7e07..25aedd6a41f 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -31,7 +31,7 @@ import { } from "./abstractions/collect-autofill-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; -class CollectAutofillContentService implements CollectAutofillContentServiceInterface { +export class CollectAutofillContentService implements CollectAutofillContentServiceInterface { private readonly sendExtensionMessage = sendExtensionMessage; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly autofillOverlayContentService: AutofillOverlayContentService; @@ -39,7 +39,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private readonly getPropertyOrAttribute = getPropertyOrAttribute; private noFieldsFound = false; private domRecentlyMutated = true; - private autofillFormElements: AutofillFormElements = new Map(); + private _autofillFormElements: AutofillFormElements = new Map(); private autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; private intersectionObserver: IntersectionObserver; @@ -79,6 +79,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte // ); } + get autofillFormElements(): AutofillFormElements { + return this._autofillFormElements; + } + /** * Builds the data for all forms and fields found within the page DOM. * Sets up a mutation observer to verify DOM changes and returns early @@ -302,14 +306,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const formElement = formElements[index] as ElementWithOpId; formElement.opid = `__form__${index}`; - const existingAutofillForm = this.autofillFormElements.get(formElement); + const existingAutofillForm = this._autofillFormElements.get(formElement); if (existingAutofillForm) { existingAutofillForm.opid = formElement.opid; - this.autofillFormElements.set(formElement, existingAutofillForm); + this._autofillFormElements.set(formElement, existingAutofillForm); continue; } - this.autofillFormElements.set(formElement, { + this._autofillFormElements.set(formElement, { opid: formElement.opid, htmlAction: this.getFormActionAttribute(formElement), htmlName: this.getPropertyOrAttribute(formElement, "name"), @@ -340,7 +344,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte */ private getFormattedAutofillFormsData(): Record { const autofillForms: Record = {}; - const autofillFormElements = Array.from(this.autofillFormElements); + const autofillFormElements = Array.from(this._autofillFormElements); for (let index = 0; index < autofillFormElements.length; index++) { const [formElement, autofillForm] = autofillFormElements[index]; autofillForms[formElement.opid] = autofillForm; @@ -1042,7 +1046,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } this.noFieldsFound = false; - this.autofillFormElements.clear(); + this._autofillFormElements.clear(); this.autofillFieldElements.clear(); this.updateAutofillElementsAfterMutation(); @@ -1178,8 +1182,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private deleteCachedAutofillElement( element: ElementWithOpId | ElementWithOpId, ) { - if (elementIsFormElement(element) && this.autofillFormElements.has(element)) { - this.autofillFormElements.delete(element); + if (elementIsFormElement(element) && this._autofillFormElements.has(element)) { + this._autofillFormElements.delete(element); return; } @@ -1216,7 +1220,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } const attributeName = mutation.attributeName?.toLowerCase(); - const autofillForm = this.autofillFormElements.get( + const autofillForm = this._autofillFormElements.get( targetElement as ElementWithOpId, ); @@ -1271,8 +1275,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } updateActions[attributeName](); - if (this.autofillFormElements.has(element)) { - this.autofillFormElements.set(element, dataTarget); + if (this._autofillFormElements.has(element)) { + this._autofillFormElements.set(element, dataTarget); } } @@ -1462,7 +1466,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. */ - private queryAllTreeWalkerNodes( + queryAllTreeWalkerNodes( rootNode: Node, filterCallback: CallableFunction, isObservingShadowRoot = true, @@ -1597,5 +1601,3 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return Boolean(this.deepQueryElements(document, `input[type="password"]`)?.length); } } - -export default CollectAutofillContentService; diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 955334e3fa0..bd03be3fccc 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -20,9 +20,11 @@ export class InlineMenuFieldQualificationService private usernameFieldTypes = new Set(["text", "email", "number", "tel"]); private usernameAutocompleteValue = "username"; private emailAutocompleteValue = "email"; + private webAuthnAutocompleteValue = "webauthn"; private loginUsernameAutocompleteValues = new Set([ this.usernameAutocompleteValue, this.emailAutocompleteValue, + this.webAuthnAutocompleteValue, ]); private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(","); private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(","); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index ff0e82d664d..3b3c487fb83 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -8,7 +8,7 @@ import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; -import CollectAutofillContentService from "./collect-autofill-content.service"; +import { CollectAutofillContentService } from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; import InsertAutofillContentService from "./insert-autofill-content.service"; diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index e475ea4bbca..f3bc01ce1fe 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -10,7 +10,7 @@ import { } from "../utils"; import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service"; -import CollectAutofillContentService from "./collect-autofill-content.service"; +import { CollectAutofillContentService } from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; class InsertAutofillContentService implements InsertAutofillContentServiceInterface { diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 2e1202b4a63..d6cca2c04f6 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -114,6 +114,7 @@ export function createGenerateFillScriptOptionsMock(customFields = {}): Generate onlyVisibleFields: false, fillNewPassword: false, allowTotpAutofill: false, + autoSubmitLogin: false, cipher: mock(), tabUrl: "https://jest-testing-website.com", defaultUriMatch: UriMatchStrategy.Domain, @@ -187,7 +188,10 @@ export function createAutofillOverlayCipherDataMock( return { id: String(index), name: `website login ${index}`, - login: { username: `username${index}` }, + login: { + username: `username${index}`, + passkey: null, + }, type: CipherType.Login, reprompt: CipherRepromptType.None, favorite: false, diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index 1cef5186028..02cb4748009 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -132,6 +132,39 @@ export function triggerWebNavigationOnCommittedEvent( ); } +export function triggerWebNavigationOnCompletedEvent( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, +) { + (chrome.webNavigation.onCompleted.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(details); + }, + ); +} + +export function triggerWebRequestOnBeforeRequestEvent( + details: chrome.webRequest.WebRequestDetails, +) { + (chrome.webRequest.onBeforeRequest.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(details); + }, + ); +} + +export function triggerWebRequestOnBeforeRedirectEvent( + details: chrome.webRequest.WebRequestDetails, +) { + ( + chrome.webRequest.onBeforeRedirect.addListener as unknown as jest.SpyInstance + ).mock.calls.forEach((call) => { + const callback = call[0]; + callback(details); + }); +} + export function mockQuerySelectorAllDefinedCall() { const originalDocumentQuerySelectorAll = document.querySelectorAll; document.querySelectorAll = function (selector: string) { diff --git a/apps/browser/src/autofill/utils/svg-icons.ts b/apps/browser/src/autofill/utils/svg-icons.ts index 1df140d37d0..eec5aaae078 100644 --- a/apps/browser/src/autofill/utils/svg-icons.ts +++ b/apps/browser/src/autofill/utils/svg-icons.ts @@ -1,19 +1,20 @@ -const logoIcon = +export const logoIcon = ''; -const logoLockedIcon = +export const logoLockedIcon = ''; -const globeIcon = +export const globeIcon = ''; -const lockIcon = +export const lockIcon = ''; -const plusIcon = +export const plusIcon = ''; -const viewCipherIcon = +export const viewCipherIcon = ''; -export { logoIcon, logoLockedIcon, globeIcon, lockIcon, plusIcon, viewCipherIcon }; +export const passkeyIcon = + ''; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index db3055b4c68..3e0e2d42e8b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -116,6 +116,7 @@ import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/ser import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/fido2/fido2-active-request-manager"; import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; @@ -142,6 +143,8 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; /* eslint-enable import/no-restricted-paths */ +import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; +import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; // eslint-disable-next-line no-restricted-imports -- Needed for service creation import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; @@ -201,6 +204,7 @@ import { } from "@bitwarden/vault-export-core"; import { OverlayBackground as OverlayBackgroundInterface } from "../autofill/background/abstractions/overlay.background"; +import { AutoSubmitLoginBackground } from "../autofill/background/auto-submit-login.background"; import ContextMenusBackground from "../autofill/background/context-menus.background"; import NotificationBackground from "../autofill/background/notification.background"; import { OverlayBackground } from "../autofill/background/overlay.background"; @@ -212,6 +216,7 @@ import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-ha import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background"; +import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import AutofillService from "../autofill/services/autofill.service"; import { SafariApp } from "../browser/safariApp"; @@ -232,17 +237,18 @@ import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service"; import { ForegroundSyncService } from "../platform/sync/foreground-sync.service"; import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; -import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -348,6 +354,7 @@ export default class MainBackground { offscreenDocumentService: OffscreenDocumentService; syncServiceListener: SyncServiceListener; themeStateService: DefaultThemeStateService; + autoSubmitLoginBackground: AutoSubmitLoginBackground; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -367,6 +374,8 @@ export default class MainBackground { private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; + private popupViewCacheBackgroundService: PopupViewCacheBackgroundService; + constructor(public popupOnlyContext: boolean = false) { // Services const lockedCallback = async (userId?: string) => { @@ -484,10 +493,15 @@ export default class MainBackground { ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage : this.memoryStorageForStateProviders; // mv2 stores to the same location + const localStorageStorageService = BrowserApi.isManifestVersion(3) + ? new OffscreenStorageService(this.offscreenDocumentService) + : new WindowStorageService(self.localStorage); + const storageServiceProvider = new BrowserStorageServiceProvider( this.storageService, this.memoryStorageForStateProviders, this.largeObjectMemoryStorageForStateProviders, + new PrimarySecondaryStorageService(this.storageService, localStorageStorageService), ); this.globalStateProvider = new DefaultGlobalStateProvider( @@ -562,6 +576,10 @@ export default class MainBackground { logoutCallback, ); + this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService( + this.globalStateProvider, + ); + const migrationRunner = new MigrationRunner( this.storageService, this.logService, @@ -984,6 +1002,7 @@ export default class MainBackground { this.syncService, this.logService, ); + const fido2ActiveRequestManager = new Fido2ActiveRequestManager(); this.fido2ClientService = new Fido2ClientService( this.fido2AuthenticatorService, this.configService, @@ -991,6 +1010,7 @@ export default class MainBackground { this.vaultSettingsService, this.domainSettingsService, this.taskSchedulerService, + fido2ActiveRequestManager, this.logService, ); @@ -1049,7 +1069,6 @@ export default class MainBackground { this.messagingService, this.appIdService, this.platformUtilsService, - this.stateService, this.logService, this.authService, this.biometricStateService, @@ -1086,6 +1105,16 @@ export default class MainBackground { this.scriptInjectorService, ); + this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( + this.logService, + this.autofillService, + this.scriptInjectorService, + this.authService, + this.configService, + this.platformUtilsService, + this.policyService, + ); + const contextMenuClickedHandler = new ContextMenuClickedHandler( (options) => this.platformUtilsService.copyToClipboard(options.text), async (_tab) => { @@ -1190,6 +1219,8 @@ export default class MainBackground { await (this.i18nService as I18nService).init(); (this.eventUploadService as EventUploadService).init(true); + this.popupViewCacheBackgroundService.startObservingTabChanges(); + if (this.popupOnlyContext) { return; } @@ -1204,6 +1235,7 @@ export default class MainBackground { await this.idleBackground.init(); this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); + await this.autoSubmitLoginBackground.init(); if ( BrowserApi.isManifestVersion(2) && @@ -1284,6 +1316,7 @@ export default class MainBackground { }), ), ); + await this.popupViewCacheBackgroundService.clearState(); await this.accountService.switchAccount(userId); await switchPromise; // Clear sequentialized caches @@ -1372,6 +1405,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService.clear(userBeingLoggedOut), this.vaultFilterService.clear(), this.biometricStateService.logout(userBeingLoggedOut), + this.popupViewCacheBackgroundService.clearState(), /* We intentionally do not clear: * - autofillSettingsService * - badgeSettingsService @@ -1536,6 +1570,7 @@ export default class MainBackground { this.autofillSettingsService, this.i18nService, this.platformUtilsService, + this.fido2ClientService, this.themeStateService, ); } diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index e19485c7118..777af9538b0 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -9,7 +9,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -21,7 +20,7 @@ import { BrowserApi } from "../platform/browser/browser-api"; import RuntimeBackground from "./runtime.background"; const MessageValidTimeout = 10 * 1000; -const EncryptionAlgorithm = "sha1"; +const HashAlgorithmForEncryption = "sha1"; type Message = { command: string; @@ -64,6 +63,7 @@ export class NativeMessagingBackground { private port: browser.runtime.Port | chrome.runtime.Port; private resolver: any = null; + private rejecter: any = null; private privateKey: Uint8Array = null; private publicKey: Uint8Array = null; private secureSetupResolve: any = null; @@ -78,7 +78,6 @@ export class NativeMessagingBackground { private messagingService: MessagingService, private appIdService: AppIdService, private platformUtilsService: PlatformUtilsService, - private stateService: StateService, private logService: LogService, private authService: AuthService, private biometricStateService: BiometricStateService, @@ -137,7 +136,7 @@ export class NativeMessagingBackground { const decrypted = await this.cryptoFunctionService.rsaDecrypt( encrypted, this.privateKey, - EncryptionAlgorithm, + HashAlgorithmForEncryption, ); if (this.validatingFingerprint) { @@ -158,19 +157,10 @@ export class NativeMessagingBackground { this.privateKey = null; this.connected = false; - this.messagingService.send("showDialog", { - title: { key: "nativeMessagingInvalidEncryptionTitle" }, - content: { key: "nativeMessagingInvalidEncryptionDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", + this.rejecter({ + message: "invalidateEncryption", }); - - if (this.resolver) { - this.resolver(message); - } - - break; + return; case "verifyFingerprint": { if (this.sharedSecret == null) { this.validatingFingerprint = true; @@ -181,8 +171,10 @@ export class NativeMessagingBackground { break; } case "wrongUserId": - this.showWrongUserDialog(); - break; + this.rejecter({ + message: "wrongUserId", + }); + return; default: // Ignore since it belongs to another device if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) { @@ -215,32 +207,12 @@ export class NativeMessagingBackground { }); } - showWrongUserDialog() { - this.messagingService.send("showDialog", { - title: { key: "nativeMessagingWrongUserTitle" }, - content: { key: "nativeMessagingWrongUserDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - } - - showIncorrectUserKeyDialog() { - this.messagingService.send("showDialog", { - title: { key: "nativeMessagingWrongUserKeyTitle" }, - content: { key: "nativeMessagingWrongUserKeyDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - } - async send(message: Message) { if (!this.connected) { await this.connect(); } - message.userId = await this.stateService.getUserId(); + message.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; message.timestamp = Date.now(); if (this.platformUtilsService.isSafari()) { @@ -260,7 +232,14 @@ export class NativeMessagingBackground { getResponse(): Promise { return new Promise((resolve, reject) => { - this.resolver = resolve; + this.resolver = function (response: any) { + resolve(response); + }; + this.rejecter = function (resp: any) { + reject({ + message: resp, + }); + }; }); } @@ -286,13 +265,7 @@ export class NativeMessagingBackground { this.privateKey = null; this.connected = false; - this.messagingService.send("showDialog", { - title: { key: "nativeMessagingInvalidEncryptionTitle" }, - content: { key: "nativeMessagingInvalidEncryptionDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); + this.rejecter("invalidateEncryption"); } } @@ -311,35 +284,11 @@ export class NativeMessagingBackground { switch (message.command) { case "biometricUnlock": { - if (message.response === "not enabled") { - this.messagingService.send("showDialog", { - title: { key: "biometricsNotEnabledTitle" }, - content: { key: "biometricsNotEnabledDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - break; - } else if (message.response === "not supported") { - this.messagingService.send("showDialog", { - title: { key: "biometricsNotSupportedTitle" }, - content: { key: "biometricsNotSupportedDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - break; - } else if (message.response === "not unlocked") { - this.messagingService.send("showDialog", { - title: { key: "biometricsNotUnlockedTitle" }, - content: { key: "biometricsNotUnlockedDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - break; - } else if (message.response === "canceled") { - break; + if ( + ["not enabled", "not supported", "not unlocked", "canceled"].includes(message.response) + ) { + this.rejecter(message.response); + return; } // Check for initial setup of biometric unlock @@ -374,12 +323,7 @@ export class NativeMessagingBackground { } else { this.logService.error("Unable to verify biometric unlocked userkey"); await this.cryptoService.clearKeys(activeUserId); - this.showIncorrectUserKeyDialog(); - - // Exit early - if (this.resolver) { - this.resolver(message); - } + this.rejecter("userkey wrong"); return; } } else { @@ -387,18 +331,18 @@ export class NativeMessagingBackground { } } catch (e) { this.logService.error("Unable to set key: " + e); - this.messagingService.send("showDialog", { - title: { key: "biometricsFailedTitle" }, - content: { key: "biometricsFailedDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); + this.rejecter("userkey wrong"); + return; + } - // Exit early - if (this.resolver) { - this.resolver(message); - } + // Verify key is correct by attempting to decrypt a secret + try { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.cryptoService.getFingerprint(userId); + } catch (e) { + this.logService.error("Unable to verify key: " + e); + await this.cryptoService.clearKeys(); + this.rejecter("userkey wrong"); return; } @@ -422,13 +366,14 @@ export class NativeMessagingBackground { const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); this.publicKey = publicKey; this.privateKey = privateKey; + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.sendUnencrypted({ command: "setupEncryption", publicKey: Utils.fromBufferToB64(publicKey), - userId: await this.stateService.getUserId(), + userId: userId, }); return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); @@ -446,7 +391,7 @@ export class NativeMessagingBackground { private async showFingerprintDialog() { const fingerprint = await this.cryptoService.getFingerprint( - await this.stateService.getUserId(), + (await firstValueFrom(this.accountService.activeAccount$))?.id, this.publicKey, ); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 8b216b9a674..33a18fbad2a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -33,6 +33,7 @@ export default class RuntimeBackground { private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsData[] = []; + private extensionRefreshIsActive: boolean = false; constructor( private main: MainBackground, @@ -89,6 +90,10 @@ export default class RuntimeBackground { return false; }; + this.extensionRefreshIsActive = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + this.messageListener.allMessages$ .pipe( mergeMap(async (message: any) => { @@ -229,6 +234,9 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(false); + if (this.extensionRefreshIsActive) { + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); + } break; } case "addToLockedVaultPendingNotifications": @@ -248,6 +256,10 @@ export default class RuntimeBackground { }, 2000); await this.configService.ensureConfigFetched(); await this.main.updateOverlayCiphers(); + + if (this.extensionRefreshIsActive) { + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); + } } break; case "openPopup": diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 111ee44d683..81f43ff82ad 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.1", + "version": "2024.8.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index e57a5522492..c6de26b8192 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.7.1", + "version": "2024.8.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/models/biometricErrors.ts b/apps/browser/src/models/biometricErrors.ts index 822a5c16f42..570c776f563 100644 --- a/apps/browser/src/models/biometricErrors.ts +++ b/apps/browser/src/models/biometricErrors.ts @@ -3,7 +3,15 @@ type BiometricError = { description: string; }; -export type BiometricErrorTypes = "startDesktop" | "desktopIntegrationDisabled"; +export type BiometricErrorTypes = + | "startDesktop" + | "desktopIntegrationDisabled" + | "not enabled" + | "not supported" + | "not unlocked" + | "invalidateEncryption" + | "userkey wrong" + | "wrongUserId"; export const BiometricErrors: Record = { startDesktop: { @@ -14,4 +22,28 @@ export const BiometricErrors: Record = { title: "desktopIntegrationDisabledTitle", description: "desktopIntegrationDisabledDesc", }, + "not enabled": { + title: "biometricsNotEnabledTitle", + description: "biometricsNotEnabledDesc", + }, + "not supported": { + title: "biometricsNotSupportedTitle", + description: "biometricsNotSupportedDesc", + }, + "not unlocked": { + title: "biometricsUnlockNotUnlockedTitle", + description: "biometricsUnlockNotUnlockedDesc", + }, + invalidateEncryption: { + title: "nativeMessagingInvalidEncryptionTitle", + description: "nativeMessagingInvalidEncryptionDesc", + }, + "userkey wrong": { + title: "nativeMessagingWrongUserKeyTitle", + description: "nativeMessagingWrongUserKeyDesc", + }, + wrongUserId: { + title: "biometricsWrongUserTitle", + description: "biometricsWrongUserDesc", + }, }; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 4994a6e9ba8..938e3191e0d 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -14,6 +14,9 @@ class OffscreenDocument implements OffscreenDocumentInterface { private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = { offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message), offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(), + localStorageGet: ({ message }) => this.handleLocalStorageGet(message.key), + localStorageSave: ({ message }) => this.handleLocalStorageSave(message.key, message.value), + localStorageRemove: ({ message }) => this.handleLocalStorageRemove(message.key), }; /** @@ -39,6 +42,18 @@ class OffscreenDocument implements OffscreenDocumentInterface { return await BrowserClipboardService.read(self); } + private handleLocalStorageGet(key: string) { + return self.localStorage.getItem(key); + } + + private handleLocalStorageSave(key: string, value: string) { + self.localStorage.setItem(key, value); + } + + private handleLocalStorageRemove(key: string) { + self.localStorage.removeItem(key); + } + /** * Sets up the listener for extension messages. */ diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html index 82a2b715a0e..fefc7154314 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.html +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -14,7 +14,7 @@ type="button" *ngIf="showBackButton" [title]="'back' | i18n" - [ariaLabel]="'back' | i18n" + [attr.aria-label]="'back' | i18n" [bitAction]="backAction" >

    diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts index 1e41f7ccbe0..fcf7f57c89f 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -1,5 +1,5 @@ import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; -import { CommonModule, Location } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { Component, Input, Signal, inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -10,6 +10,8 @@ import { TypographyModule, } from "@bitwarden/components"; +import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; + import { PopupPageComponent } from "./popup-page.component"; @Component({ @@ -19,6 +21,7 @@ import { PopupPageComponent } from "./popup-page.component"; imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule], }) export class PopupHeaderComponent { + private popupRouterCacheService = inject(PopupRouterCacheService); protected pageContentScrolled: Signal = inject(PopupPageComponent).isScrolled; /** Background color */ @@ -46,8 +49,6 @@ export class PopupHeaderComponent { **/ @Input() backAction: FunctionReturningAwaitable = async () => { - this.location.back(); + return this.popupRouterCacheService.back(); }; - - constructor(private location: Location) {} } diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 3d067b53056..affa804cc79 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -16,6 +16,8 @@ import { SectionComponent, } from "@bitwarden/components"; +import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; + import { PopupFooterComponent } from "./popup-footer.component"; import { PopupHeaderComponent } from "./popup-header.component"; import { PopupPageComponent } from "./popup-page.component"; @@ -334,6 +336,12 @@ export default { { useHash: true }, ), ), + { + provide: PopupRouterCacheService, + useValue: { + back() {}, + } as Partial, + }, ], }), ], diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts new file mode 100644 index 00000000000..52b08ac09ab --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts @@ -0,0 +1,131 @@ +import { Location } from "@angular/common"; +import { Injectable, inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + NavigationEnd, + Router, + UrlSerializer, +} from "@angular/router"; +import { filter, first, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; + +import { POPUP_ROUTE_HISTORY_KEY } from "../../../platform/services/popup-view-cache-background.service"; +import BrowserPopupUtils from "../browser-popup-utils"; + +/** + * Preserves route history when opening and closing the popup + * + * Routes marked with `doNotSaveUrl` will not be stored + **/ +@Injectable({ + providedIn: "root", +}) +export class PopupRouterCacheService { + private router = inject(Router); + private state = inject(GlobalStateProvider).get(POPUP_ROUTE_HISTORY_KEY); + private location = inject(Location); + + constructor() { + // init history with existing state + this.history$() + .pipe(first()) + .subscribe((history) => history.forEach((location) => this.location.go(location))); + + // update state when route change occurs + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + filter((_event: NavigationEnd) => { + const state: ActivatedRouteSnapshot = this.router.routerState.snapshot.root; + + let child = state.firstChild; + while (child.firstChild) { + child = child.firstChild; + } + + return !child?.data?.doNotSaveUrl ?? true; + }), + switchMap((event) => this.push(event.url)), + ) + .subscribe(); + } + + history$(): Observable { + return this.state.state$; + } + + async setHistory(state: string[]): Promise { + return this.state.update(() => state); + } + + /** Get the last item from the history stack, or `null` if empty */ + last$(): Observable { + return this.history$().pipe( + map((history) => { + if (!history || history.length === 0) { + return null; + } + return history[history.length - 1]; + }), + ); + } + + /** + * If in browser popup, push new route onto history stack + */ + private async push(url: string): Promise { + if (!BrowserPopupUtils.inPopup(window) || url === (await firstValueFrom(this.last$()))) { + return; + } + await this.state.update((prevState) => (prevState == null ? [url] : prevState.concat(url))); + } + + /** + * Navigate back in history + */ + async back() { + await this.state.update((prevState) => prevState.slice(0, -1)); + + const url = this.router.url; + this.location.back(); + if (url !== this.router.url) { + return; + } + + // if no history is present, fallback to vault page + await this.router.navigate([""]); + } +} + +/** + * Redirect to the last visited route. Should be applied to root route. + * + * If `FeatureFlag.PersistPopupView` is disabled, do nothing. + **/ +export const popupRouterCacheGuard = (() => { + const configService = inject(ConfigService); + const popupHistoryService = inject(PopupRouterCacheService); + const urlSerializer = inject(UrlSerializer); + + return configService.getFeatureFlag$(FeatureFlag.PersistPopupView).pipe( + switchMap((featureEnabled) => { + if (!featureEnabled) { + return of(true); + } + + return popupHistoryService.last$().pipe( + map((url: string) => { + if (!url) { + return true; + } + + return urlSerializer.parse(url); + }), + ); + }), + ); +}) satisfies CanActivateFn; diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts new file mode 100644 index 00000000000..dde7f10500b --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts @@ -0,0 +1,113 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router, UrlSerializer, UrlTree } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { FakeGlobalStateProvider } from "@bitwarden/common/spec"; + +import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-cache.service"; + +const flushPromises = async () => await new Promise(process.nextTick); + +@Component({ template: "" }) +export class EmptyComponent {} + +describe("Popup router cache guard", () => { + const configServiceMock = mock(); + const fakeGlobalStateProvider = new FakeGlobalStateProvider(); + + let testBed: TestBed; + let serializer: UrlSerializer; + let router: Router; + + let service: PopupRouterCacheService; + + beforeEach(async () => { + jest.spyOn(configServiceMock, "getFeatureFlag$").mockReturnValue(of(true)); + + testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "a", component: EmptyComponent }, + { path: "b", component: EmptyComponent }, + { + path: "c", + component: EmptyComponent, + data: { doNotSaveUrl: true }, + }, + ]), + ], + providers: [ + { provide: ConfigService, useValue: configServiceMock }, + { provide: GlobalStateProvider, useValue: fakeGlobalStateProvider }, + ], + }); + + await testBed.compileComponents(); + + router = testBed.inject(Router); + serializer = testBed.inject(UrlSerializer); + + service = testBed.inject(PopupRouterCacheService); + + await service.setHistory([]); + }); + + it("returns true if the history stack is empty", async () => { + const response = await firstValueFrom( + testBed.runInInjectionContext(() => popupRouterCacheGuard()), + ); + + expect(response).toBe(true); + }); + + it("redirects to the latest stored route", async () => { + await router.navigate(["a"]); + await router.navigate(["b"]); + + const response = (await firstValueFrom( + testBed.runInInjectionContext(() => popupRouterCacheGuard()), + )) as UrlTree; + + expect(serializer.serialize(response)).toBe("/b"); + }); + + it("back method redirects to the previous route", async () => { + await router.navigate(["a"]); + await router.navigate(["b"]); + + // wait for router events subscription + await flushPromises(); + + expect(await firstValueFrom(service.history$())).toEqual(["/a", "/b"]); + + await service.back(); + + expect(await firstValueFrom(service.history$())).toEqual(["/a"]); + }); + + it("does not save ignored routes", async () => { + await router.navigate(["a"]); + await router.navigate(["b"]); + await router.navigate(["c"]); + + const response = (await firstValueFrom( + testBed.runInInjectionContext(() => popupRouterCacheGuard()), + )) as UrlTree; + + expect(serializer.serialize(response)).toBe("/b"); + }); + + it("does not save duplicate routes", async () => { + await router.navigate(["a"]); + await router.navigate(["a"]); + + await flushPromises(); + + expect(await firstValueFrom(service.history$())).toEqual(["/a"]); + }); +}); diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 4690562fd75..47a128bc1bc 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -74,8 +74,12 @@ export default abstract class AbstractChromeStorageService } async get(key: string): Promise { - return new Promise((resolve) => { - this.chromeStorageApi.get(key, (obj: any) => { + return new Promise((resolve, reject) => { + this.chromeStorageApi.get(key, (obj) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + if (obj != null && obj[key] != null) { resolve(this.processGetObject(obj[key])); return; @@ -98,16 +102,24 @@ export default abstract class AbstractChromeStorageService } const keyedObj = { [key]: obj }; - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.chromeStorageApi.set(keyedObj, () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(); }); }); } async remove(key: string): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.chromeStorageApi.remove(key, () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(); }); }); diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index ceadc16a58e..ac8a01375fa 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -51,6 +51,10 @@ describe("ChromeStorageApiService", () => { }); }); + afterEach(() => { + chrome.runtime.lastError = undefined; + }); + it("uses `objToStore` to prepare a value for set", async () => { const key = "key"; const value = { key: "value" }; @@ -73,6 +77,15 @@ describe("ChromeStorageApiService", () => { await service.save(key, null); expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function)); }); + + it("translates chrome.runtime.lastError to promise rejection", async () => { + setMock.mockImplementation((data, callback) => { + chrome.runtime.lastError = new Error("Test Error"); + callback(); + }); + + await expect(async () => await service.save("test", {})).rejects.toThrow("Test Error"); + }); }); describe("get", () => { @@ -87,6 +100,10 @@ describe("ChromeStorageApiService", () => { }); }); + afterEach(() => { + chrome.runtime.lastError = undefined; + }); + it("returns a stored value when it is serialized", async () => { const value = { key: "value" }; store[key] = objToStore(value); @@ -112,5 +129,15 @@ describe("ChromeStorageApiService", () => { const result = await service.get(key); expect(result).toBeNull(); }); + + it("translates chrome.runtime.lastError to promise rejection", async () => { + getMock.mockImplementation((key, callback) => { + chrome.runtime.lastError = new Error("Test Error"); + callback(); + chrome.runtime.lastError = undefined; + }); + + await expect(async () => await service.get("test")).rejects.toThrow("Test Error"); + }); }); }); diff --git a/apps/browser/src/platform/services/browser-local-storage.service.spec.ts b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts index bd79dd6fa56..13e26c26ddd 100644 --- a/apps/browser/src/platform/services/browser-local-storage.service.spec.ts +++ b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts @@ -62,6 +62,7 @@ describe("BrowserLocalStorageService", () => { }); afterEach(() => { + chrome.runtime.lastError = undefined; jest.resetAllMocks(); }); @@ -121,6 +122,24 @@ describe("BrowserLocalStorageService", () => { expect(clearMock).toHaveBeenCalledTimes(1); }); + + it("throws if get has chrome.runtime.lastError", async () => { + getMock.mockImplementation((key, callback) => { + chrome.runtime.lastError = new Error("Get Test Error"); + callback(); + }); + + await expect(async () => await service.reseed()).rejects.toThrow("Get Test Error"); + }); + + it("throws if save has chrome.runtime.lastError", async () => { + saveMock.mockImplementation((obj, callback) => { + chrome.runtime.lastError = new Error("Save Test Error"); + callback(); + }); + + await expect(async () => await service.reseed()).rejects.toThrow("Save Test Error"); + }); }); describe.each(["get", "has", "save", "remove"] as const)("%s", (method) => { diff --git a/apps/browser/src/platform/services/browser-local-storage.service.ts b/apps/browser/src/platform/services/browser-local-storage.service.ts index 7957b6edeaf..0ba200055bb 100644 --- a/apps/browser/src/platform/services/browser-local-storage.service.ts +++ b/apps/browser/src/platform/services/browser-local-storage.service.ts @@ -81,8 +81,11 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer * Clears local storage */ private async clear() { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.chromeStorageApi.clear(() => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } resolve(); }); }); @@ -95,8 +98,12 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer * @returns Promise resolving to keyed object of all stored data */ private async getAll(): Promise> { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { this.chromeStorageApi.get(null, (allStorage) => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + const resolved = Object.entries(allStorage).reduce( (agg, [key, value]) => { agg[key] = this.processGetObject(value); @@ -110,7 +117,7 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer } private async saveAll(data: Record): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const keyedData = Object.entries(data).reduce( (agg, [key, value]) => { agg[key] = objToStore(value); @@ -119,6 +126,10 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer {} as Record, ); this.chromeStorageApi.set(keyedData, () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 2c14ac2833d..28abb78b19c 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -8,6 +8,7 @@ import { ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { compareValues } from "@bitwarden/common/platform/misc/compare-values"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; @@ -190,23 +191,7 @@ export class LocalBackedSessionStorageService private compareValues(value1: T, value2: T): boolean { try { - if (value1 == null && value2 == null) { - return true; - } - - if (value1 && value2 == null) { - return false; - } - - if (value1 == null && value2) { - return false; - } - - if (typeof value1 !== "object" || typeof value2 !== "object") { - return value1 === value2; - } - - return JSON.stringify(value1) === JSON.stringify(value2); + return compareValues(value1, value2); } catch (e) { this.logService.error( `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, diff --git a/apps/browser/src/platform/services/popup-view-cache-background.service.ts b/apps/browser/src/platform/services/popup-view-cache-background.service.ts new file mode 100644 index 00000000000..3427a02d3fa --- /dev/null +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -0,0 +1,63 @@ +import { switchMap, merge, delay, filter, map } from "rxjs"; + +import { + POPUP_VIEW_MEMORY, + KeyDefinition, + GlobalStateProvider, +} from "@bitwarden/common/platform/state"; + +import { fromChromeEvent } from "../browser/from-chrome-event"; + +const popupClosedPortName = "new_popup"; + +export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition( + POPUP_VIEW_MEMORY, + "popup-route-history", + { + deserializer: (jsonValue) => jsonValue, + }, +); + +export class PopupViewCacheBackgroundService { + private popupRouteHistoryState = this.globalStateProvider.get(POPUP_ROUTE_HISTORY_KEY); + + constructor(private globalStateProvider: GlobalStateProvider) {} + + startObservingTabChanges() { + merge( + // on tab changed, excluding extension tabs + fromChromeEvent(chrome.tabs.onActivated).pipe( + switchMap(([tabInfo]) => chrome.tabs.get(tabInfo.tabId)), + map((tab) => tab.url || tab.pendingUrl), + filter((url) => !url.startsWith(chrome.runtime.getURL(""))), + ), + + // on popup closed, with 2 minute delay that is cancelled by re-opening the popup + fromChromeEvent(chrome.runtime.onConnect).pipe( + filter(([port]) => port.name === popupClosedPortName), + switchMap(([port]) => fromChromeEvent(port.onDisconnect).pipe(delay(1000 * 60 * 2))), + ), + ) + .pipe(switchMap(() => this.clearState())) + .subscribe(); + } + + async clearState() { + return Promise.all([ + this.popupRouteHistoryState.update(() => [], { shouldUpdate: this.objNotEmpty }), + ]); + } + + private objNotEmpty(obj: object): boolean { + return Object.keys(obj ?? {}).length !== 0; + } +} + +/** + * Communicates to {@link PopupViewCacheBackgroundService} that the extension popup has been closed. + * + * Call in the foreground. + **/ +export const initPopupClosedListener = () => { + chrome.runtime.connect({ name: popupClosedPortName }); +}; diff --git a/apps/browser/src/platform/storage/browser-storage-service.provider.ts b/apps/browser/src/platform/storage/browser-storage-service.provider.ts index e0214baef44..5854669138a 100644 --- a/apps/browser/src/platform/storage/browser-storage-service.provider.ts +++ b/apps/browser/src/platform/storage/browser-storage-service.provider.ts @@ -14,6 +14,7 @@ export class BrowserStorageServiceProvider extends StorageServiceProvider { diskStorageService: AbstractStorageService & ObservableStorageService, limitedMemoryStorageService: AbstractStorageService & ObservableStorageService, private largeObjectMemoryStorageService: AbstractStorageService & ObservableStorageService, + private readonly diskBackupLocalStorage: AbstractStorageService & ObservableStorageService, ) { super(diskStorageService, limitedMemoryStorageService); } @@ -26,6 +27,8 @@ export class BrowserStorageServiceProvider extends StorageServiceProvider { switch (location) { case "memory-large-object": return ["memory-large-object", this.largeObjectMemoryStorageService]; + case "disk-backup-local-storage": + return ["disk-backup-local-storage", this.diskBackupLocalStorage]; default: // Pass in computed location to super because they could have // override default "disk" with web "memory". diff --git a/apps/browser/src/platform/storage/offscreen-storage.service.ts b/apps/browser/src/platform/storage/offscreen-storage.service.ts new file mode 100644 index 00000000000..34d3bd7a9ac --- /dev/null +++ b/apps/browser/src/platform/storage/offscreen-storage.service.ts @@ -0,0 +1,55 @@ +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; + +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +export class OffscreenStorageService implements AbstractStorageService { + constructor(private readonly offscreenDocumentService: OffscreenDocumentService) {} + + get valuesRequireDeserialization(): boolean { + return true; + } + + async get(key: string, options?: StorageOptions): Promise { + return await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.LOCAL_STORAGE], + "backup storage of user data", + async () => { + const response = await BrowserApi.sendMessageWithResponse("localStorageGet", { + key, + }); + if (response != null) { + return JSON.parse(response); + } + + return response; + }, + ); + } + async has(key: string, options?: StorageOptions): Promise { + return (await this.get(key, options)) != null; + } + + async save(key: string, obj: T, options?: StorageOptions): Promise { + await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.LOCAL_STORAGE], + "backup storage of user data", + async () => + await BrowserApi.sendMessageWithResponse("localStorageSave", { + key, + value: JSON.stringify(obj), + }), + ); + } + async remove(key: string, options?: StorageOptions): Promise { + await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.LOCAL_STORAGE], + "backup storage of user data", + async () => + await BrowserApi.sendMessageWithResponse("localStorageRemove", { + key, + }), + ); + } +} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index e556e459287..5c4c01bfc16 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -48,6 +48,7 @@ import { NotificationsSettingsV1Component } from "../autofill/popup/settings/not import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumComponent } from "../billing/popup/settings/premium.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; +import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; @@ -64,6 +65,7 @@ import { ImportBrowserV2Component } from "../tools/popup/settings/import/import- import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { SettingsComponent } from "../tools/popup/settings/settings.component"; +import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; import { CollectionsComponent } from "../vault/popup/components/vault/collections.component"; @@ -78,8 +80,10 @@ import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/ import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; +import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { SyncComponent } from "../vault/popup/settings/sync.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; @@ -102,6 +106,7 @@ const routes: Routes = [ pathMatch: "full", children: [], // Children lets us have an empty component. canActivate: [ + popupRouterCacheGuard, redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }), ], }, @@ -302,12 +307,11 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "vault-settings" }, }), - { + ...extensionRefreshSwap(FoldersComponent, FoldersV2Component, { path: "folders", - component: FoldersComponent, canActivate: [authGuard], data: { state: "folders" }, - }, + }), { path: "add-folder", component: FolderAddEditComponent, @@ -338,12 +342,11 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "premium" }, }, - { + ...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, { path: "appearance", - component: AppearanceComponent, canActivate: [authGuard], data: { state: "appearance" }, - }, + }), ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "clone-cipher", canActivate: [authGuard], @@ -457,6 +460,7 @@ const routes: Routes = [ ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", canActivate: [authGuard], + canDeactivate: [clearVaultStateGuard], data: { state: "tabs_vault" }, }), { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 287e9096684..7ac3e02160d 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -20,6 +20,7 @@ import { } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; +import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -59,6 +60,8 @@ export class AppComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { + initPopupClosedListener(); + // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur await this.clearComponentStates(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 2fba41d17ad..f6f3bf732b0 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -73,6 +73,8 @@ import { } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Used for dependency injection import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state"; +import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; +import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -122,6 +124,10 @@ const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE"); +const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken< + AbstractStorageService & ObservableStorageService +>("DISK_BACKUP_LOCAL_STORAGE"); + const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); const mainBackground: MainBackground = needsBackgroundInit ? createLocalBgService() @@ -496,6 +502,12 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: DISK_BACKUP_LOCAL_STORAGE, + useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) => + new PrimarySecondaryStorageService(diskStorage, new WindowStorageService(self.localStorage)), + deps: [OBSERVABLE_DISK_STORAGE], + }), safeProvider({ provide: StorageServiceProvider, useClass: BrowserStorageServiceProvider, @@ -503,6 +515,7 @@ const safeProviders: SafeProvider[] = [ OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + DISK_BACKUP_LOCAL_STORAGE, ], }), safeProvider({ 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 53a0441eecd..50e5531743a 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 @@ -34,6 +34,7 @@ import { CurrentAccountComponent } from "../../../auth/popup/account-switching/c 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 { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { SendV2Component, SendState } from "./send-v2.component"; @@ -102,6 +103,7 @@ describe("SendV2Component", () => { { provide: SendItemsService, useValue: sendItemsService }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: SendListFiltersService, useValue: sendListFiltersService }, + { provide: PopupRouterCacheService, useValue: mock() }, ], }).compileComponents(); diff --git a/apps/browser/src/vault/guards/clear-vault-state.guard.ts b/apps/browser/src/vault/guards/clear-vault-state.guard.ts new file mode 100644 index 00000000000..2b43f1ecbd3 --- /dev/null +++ b/apps/browser/src/vault/guards/clear-vault-state.guard.ts @@ -0,0 +1,28 @@ +import { inject } from "@angular/core"; +import { CanDeactivateFn } from "@angular/router"; + +import { VaultV2Component } from "../popup/components/vault/vault-v2.component"; +import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service"; +import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service"; + +/** + * Guard to clear the vault state (search and filter) when navigating away from the vault view. + * This ensures the search and filter state is reset when navigating between different tabs, except viewing a cipher. + */ +export const clearVaultStateGuard: CanDeactivateFn = ( + component: VaultV2Component, + currentRoute, + currentState, + nextState, +) => { + const vaultPopupItemsService = inject(VaultPopupItemsService); + const vaultPopupListFiltersService = inject(VaultPopupListFiltersService); + if (nextState && !isViewingCipher(nextState.url)) { + vaultPopupItemsService.applyFilter(""); + vaultPopupListFiltersService.resetFilterForm(); + } + + return true; +}; + +const isViewingCipher = (url: string): boolean => url.includes("view-cipher"); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html new file mode 100644 index 00000000000..0e6dbf24427 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html @@ -0,0 +1,41 @@ +
    + + + {{ (variant === "add" ? "newFolder" : "editFolder") | i18n }} + +
    + + {{ "folderName" | i18n }} + + + {{ "folderHintText" | i18n }} + + +
    +
    + + + + +
    +
    +
    diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts new file mode 100644 index 00000000000..8453b4cc63e --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts @@ -0,0 +1,157 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { + AddEditFolderDialogComponent, + AddEditFolderDialogData, +} from "./add-edit-folder-dialog.component"; + +describe("AddEditFolderDialogComponent", () => { + let component: AddEditFolderDialogComponent; + let fixture: ComponentFixture; + + const dialogData = {} as AddEditFolderDialogData; + const folder = new Folder(); + const encrypt = jest.fn().mockResolvedValue(folder); + const save = jest.fn().mockResolvedValue(null); + const deleteFolder = jest.fn().mockResolvedValue(null); + const openSimpleDialog = jest.fn().mockResolvedValue(true); + const error = jest.fn(); + const close = jest.fn(); + const showToast = jest.fn(); + + const dialogRef = { + close, + }; + + beforeEach(async () => { + encrypt.mockClear(); + save.mockClear(); + deleteFolder.mockClear(); + error.mockClear(); + close.mockClear(); + showToast.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AddEditFolderDialogComponent, NoopAnimationsModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: FolderService, useValue: { encrypt } }, + { provide: FolderApiServiceAbstraction, useValue: { save, delete: deleteFolder } }, + { provide: LogService, useValue: { error } }, + { provide: ToastService, useValue: { showToast } }, + { provide: DIALOG_DATA, useValue: dialogData }, + { provide: DialogRef, useValue: dialogRef }, + ], + }) + .overrideProvider(DialogService, { useValue: { openSimpleDialog } }) + .compileComponents(); + + fixture = TestBed.createComponent(AddEditFolderDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("new folder", () => { + it("requires a folder name", async () => { + await component.submit(); + + expect(encrypt).not.toHaveBeenCalled(); + + component.folderForm.controls.name.setValue("New Folder"); + + await component.submit(); + + expect(encrypt).toHaveBeenCalled(); + }); + + it("submits a new folder view", async () => { + component.folderForm.controls.name.setValue("New Folder"); + + await component.submit(); + + const newFolder = new FolderView(); + newFolder.name = "New Folder"; + + expect(encrypt).toHaveBeenCalledWith(newFolder); + expect(save).toHaveBeenCalled(); + }); + + it("shows success toast after saving", async () => { + component.folderForm.controls.name.setValue("New Folder"); + + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + message: "editedFolder", + title: null, + variant: "success", + }); + }); + + it("closes the dialog after saving", async () => { + component.folderForm.controls.name.setValue("New Folder"); + + await component.submit(); + + expect(close).toHaveBeenCalled(); + }); + + it("logs error if saving fails", async () => { + const errorObj = new Error("Failed to save folder"); + save.mockRejectedValue(errorObj); + + component.folderForm.controls.name.setValue("New Folder"); + + await component.submit(); + + expect(error).toHaveBeenCalledWith(errorObj); + }); + }); + + describe("editing folder", () => { + const folderView = new FolderView(); + folderView.id = "1"; + folderView.name = "Folder 1"; + + beforeEach(() => { + dialogData.editFolderConfig = { folder: folderView }; + + component.ngOnInit(); + }); + + it("populates form with folder name", () => { + expect(component.folderForm.controls.name.value).toBe("Folder 1"); + }); + + it("submits the updated folder", async () => { + component.folderForm.controls.name.setValue("Edited Folder"); + await component.submit(); + + expect(encrypt).toHaveBeenCalledWith({ + ...dialogData.editFolderConfig.folder, + name: "Edited Folder", + }); + }); + + it("deletes the folder", async () => { + await component.deleteFolder(); + + expect(deleteFolder).toHaveBeenCalledWith(folderView.id); + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "deletedFolder", + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts new file mode 100644 index 00000000000..33263533990 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -0,0 +1,155 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + Component, + DestroyRef, + inject, + Inject, + OnInit, + ViewChild, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + AsyncActionsModule, + BitSubmitDirective, + ButtonComponent, + ButtonModule, + DialogModule, + DialogService, + FormFieldModule, + IconButtonModule, + ToastService, +} from "@bitwarden/components"; + +export type AddEditFolderDialogData = { + /** When provided, dialog will display edit folder variant */ + editFolderConfig?: { folder: FolderView }; +}; + +@Component({ + standalone: true, + selector: "vault-add-edit-folder-dialog", + templateUrl: "./add-edit-folder-dialog.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + FormFieldModule, + ReactiveFormsModule, + IconButtonModule, + AsyncActionsModule, + ], +}) +export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { + @ViewChild(BitSubmitDirective) private bitSubmit: BitSubmitDirective; + @ViewChild("submitBtn") private submitBtn: ButtonComponent; + + folder: FolderView; + + variant: "add" | "edit"; + + folderForm = this.formBuilder.group({ + name: ["", Validators.required], + }); + + private destroyRef = inject(DestroyRef); + + constructor( + private formBuilder: FormBuilder, + private folderService: FolderService, + private folderApiService: FolderApiServiceAbstraction, + private toastService: ToastService, + private i18nService: I18nService, + private logService: LogService, + private dialogService: DialogService, + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) private data?: AddEditFolderDialogData, + ) {} + + ngOnInit(): void { + this.variant = this.data?.editFolderConfig ? "edit" : "add"; + + if (this.variant === "edit") { + this.folderForm.controls.name.setValue(this.data.editFolderConfig.folder.name); + this.folder = this.data.editFolderConfig.folder; + } else { + // Create a new folder view + this.folder = new FolderView(); + } + } + + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.loading = loading; + }); + } + + /** Submit the new folder */ + submit = async () => { + if (this.folderForm.invalid) { + return; + } + + this.folder.name = this.folderForm.controls.name.value; + + try { + const folder = await this.folderService.encrypt(this.folder); + await this.folderApiService.save(folder); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedFolder"), + }); + + this.close(); + } catch (e) { + this.logService.error(e); + } + }; + + /** Delete the folder with when the user provides a confirmation */ + deleteFolder = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteFolder" }, + content: { key: "deleteFolderPermanently" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.folderApiService.delete(this.folder.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedFolder"), + }); + } catch (e) { + this.logService.error(e); + } + + this.close(); + }; + + /** Close the dialog */ + private close() { + this.dialogRef.close(); + } +} 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 cae324fac1f..1c4519f4307 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 @@ -14,19 +14,21 @@ import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/compo import { CipherFormConfig, CipherFormConfigService, + CipherFormGenerationService, CipherFormMode, CipherFormModule, DefaultCipherFormConfigService, TotpCaptureService, } from "@bitwarden/vault"; +import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service"; -import { BrowserFido2UserInterfaceSession } from "../../../../fido2/browser-fido2-user-interface.service"; +import { BrowserCipherFormGenerationService } from "../../../services/browser-cipher-form-generation.service"; import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { fido2PopoutSessionData$, @@ -106,6 +108,7 @@ export type AddEditQueryParams = Partial>; providers: [ { provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }, { provide: TotpCaptureService, useClass: BrowserTotpCaptureService }, + { provide: CipherFormGenerationService, useClass: BrowserCipherFormGenerationService }, ], imports: [ CommonModule, @@ -225,7 +228,10 @@ export class AddEditV2Component implements OnInit { return; } - this.location.back(); + await this.router.navigate(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: cipher.id }, + }); } subscribeToParams(): void { 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 0291d6e872d..4857703d3b1 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 @@ -76,13 +76,6 @@ export class ItemMoreOptionsComponent { } async doAutofillAndSave() { - if ( - this.cipher.reprompt === CipherRepromptType.Password && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - return; - } - await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index 0bd85c21696..78403784f46 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -3,20 +3,25 @@ {{ "new" | i18n }} - + {{ "typeLogin" | i18n }} - + {{ "typeCard" | i18n }} - + {{ "typeIdentity" | i18n }} - + {{ "note" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts new file mode 100644 index 00000000000..868cb242aa2 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -0,0 +1,108 @@ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components"; + +import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component"; + +import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component"; + +describe("NewItemDropdownV2Component", () => { + let component: NewItemDropdownV2Component; + let fixture: ComponentFixture; + const open = jest.fn(); + const navigate = jest.fn(); + + beforeEach(async () => { + open.mockClear(); + navigate.mockClear(); + + await TestBed.configureTestingModule({ + imports: [NewItemDropdownV2Component, MenuModule, ButtonModule, JslibModule, CommonModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: Router, useValue: { navigate } }, + ], + }) + .overrideProvider(DialogService, { useValue: { open } }) + .compileComponents(); + + fixture = TestBed.createComponent(NewItemDropdownV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("opens new folder dialog", () => { + component.openFolderDialog(); + + expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent); + }); + + describe("new item", () => { + const emptyParams: AddEditQueryParams = { + collectionId: undefined, + organizationId: undefined, + folderId: undefined, + }; + + beforeEach(() => { + jest.spyOn(component, "newItemNavigate"); + }); + + it("navigates to new login", () => { + component.newItemNavigate(CipherType.Login); + + expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { + queryParams: { type: CipherType.Login.toString(), ...emptyParams }, + }); + }); + + it("navigates to new card", () => { + component.newItemNavigate(CipherType.Card); + + expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { + queryParams: { type: CipherType.Card.toString(), ...emptyParams }, + }); + }); + + it("navigates to new identity", () => { + component.newItemNavigate(CipherType.Identity); + + expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { + queryParams: { type: CipherType.Identity.toString(), ...emptyParams }, + }); + }); + + it("navigates to new note", () => { + component.newItemNavigate(CipherType.SecureNote); + + expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { + queryParams: { type: CipherType.SecureNote.toString(), ...emptyParams }, + }); + }); + + it("includes initial values", () => { + component.initialValues = { + folderId: "222-333-444", + organizationId: "444-555-666", + collectionId: "777-888-999", + } as NewItemInitialValues; + + component.newItemNavigate(CipherType.Login); + + expect(navigate).toHaveBeenCalledWith(["/add-cipher"], { + queryParams: { + type: CipherType.Login.toString(), + folderId: "222-333-444", + organizationId: "444-555-666", + collectionId: "777-888-999", + }, + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index 65456fd74ae..daa0f3d795c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -5,14 +5,16 @@ import { Router, RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, MenuModule, NoItemsModule } from "@bitwarden/components"; +import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component"; export interface NewItemInitialValues { folderId?: string; organizationId?: OrganizationId; collectionId?: CollectionId; + uri?: string; } @Component({ @@ -30,7 +32,10 @@ export class NewItemDropdownV2Component { @Input() initialValues: NewItemInitialValues; - constructor(private router: Router) {} + constructor( + private router: Router, + private dialogService: DialogService, + ) {} private buildQueryParams(type: CipherType): AddEditQueryParams { return { @@ -38,10 +43,15 @@ export class NewItemDropdownV2Component { collectionId: this.initialValues?.collectionId, organizationId: this.initialValues?.organizationId, folderId: this.initialValues?.folderId, + uri: this.initialValues?.uri, }; } newItemNavigate(type: CipherType) { void this.router.navigate(["/add-cipher"], { queryParams: this.buildQueryParams(type) }); } + + openFolderDialog() { + this.dialogService.open(AddEditFolderDialogComponent); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html new file mode 100644 index 00000000000..7652b8ab0bf --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.html @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts new file mode 100644 index 00000000000..d37bc367110 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts @@ -0,0 +1,85 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherFormGeneratorComponent } from "@bitwarden/vault"; + +import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; + +import { + GeneratorDialogParams, + GeneratorDialogResult, + VaultGeneratorDialogComponent, +} from "./vault-generator-dialog.component"; + +@Component({ + selector: "vault-cipher-form-generator", + template: "", + standalone: true, +}) +class MockCipherFormGenerator { + @Input() type: "password" | "username"; + @Output() valueGenerated = new EventEmitter(); +} + +describe("VaultGeneratorDialogComponent", () => { + let component: VaultGeneratorDialogComponent; + let fixture: ComponentFixture; + let mockDialogRef: MockProxy>; + let dialogData: GeneratorDialogParams; + + beforeEach(async () => { + mockDialogRef = mock>(); + dialogData = { type: "password" }; + + await TestBed.configureTestingModule({ + imports: [VaultGeneratorDialogComponent, NoopAnimationsModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DIALOG_DATA, useValue: dialogData }, + { provide: DialogRef, useValue: mockDialogRef }, + { provide: PopupRouterCacheService, useValue: mock() }, + ], + }) + .overrideComponent(VaultGeneratorDialogComponent, { + remove: { imports: [CipherFormGeneratorComponent] }, + add: { imports: [MockCipherFormGenerator] }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(VaultGeneratorDialogComponent); + component = fixture.componentInstance; + }); + + it("should create", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("should use the appropriate text based on generator type", () => { + expect(component["title"]).toBe("passwordGenerator"); + expect(component["selectButtonText"]).toBe("useThisPassword"); + + dialogData.type = "username"; + + fixture = TestBed.createComponent(VaultGeneratorDialogComponent); + component = fixture.componentInstance; + + expect(component["title"]).toBe("usernameGenerator"); + expect(component["selectButtonText"]).toBe("useThisUsername"); + }); + + it("should close the dialog with the generated value when the user selects it", () => { + component["generatedValue"] = "generated-value"; + + fixture.nativeElement.querySelector("button[data-testid='select-button']").click(); + + expect(mockDialogRef.close).toHaveBeenCalledWith({ + action: "selected", + generatedValue: "generated-value", + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts new file mode 100644 index 00000000000..657d126081f --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts @@ -0,0 +1,120 @@ +import { animate, group, style, transition, trigger } from "@angular/animations"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Overlay } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ButtonModule, DialogService } from "@bitwarden/components"; +import { CipherFormGeneratorComponent } from "@bitwarden/vault"; + +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; + +export interface GeneratorDialogParams { + type: "password" | "username"; +} + +export interface GeneratorDialogResult { + action: GeneratorDialogAction; + generatedValue?: string; +} + +export enum GeneratorDialogAction { + Selected = "selected", + Canceled = "canceled", +} + +const slideIn = trigger("slideIn", [ + transition(":enter", [ + style({ opacity: 0, transform: "translateY(100vh)" }), + group([ + animate("0.15s linear", style({ opacity: 1 })), + animate("0.3s ease-out", style({ transform: "none" })), + ]), + ]), +]); + +@Component({ + selector: "app-vault-generator-dialog", + templateUrl: "./vault-generator-dialog.component.html", + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + CommonModule, + CipherFormGeneratorComponent, + ButtonModule, + ], + animations: [slideIn], +}) +export class VaultGeneratorDialogComponent { + protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator"); + protected selectButtonText = this.i18nService.t( + this.isPassword ? "useThisPassword" : "useThisUsername", + ); + + /** + * Whether the dialog is generating a password/passphrase. If false, it is generating a username. + * @protected + */ + protected get isPassword() { + return this.params.type === "password"; + } + + /** + * The currently generated value. + * @protected + */ + protected generatedValue: string = ""; + + constructor( + @Inject(DIALOG_DATA) protected params: GeneratorDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + ) {} + + /** + * Close the dialog without selecting a value. + */ + protected close = () => { + this.dialogRef.close({ action: GeneratorDialogAction.Canceled }); + }; + + /** + * Close the dialog and select the currently generated value. + */ + protected selectValue = () => { + this.dialogRef.close({ + action: GeneratorDialogAction.Selected, + generatedValue: this.generatedValue, + }); + }; + + onValueGenerated(value: string) { + this.generatedValue = value; + } + + /** + * Opens the vault generator dialog in a full screen dialog. + */ + static open( + dialogService: DialogService, + overlay: Overlay, + config: DialogConfig, + ) { + const position = overlay.position().global(); + + return dialogService.open( + VaultGeneratorDialogComponent, + { + ...config, + positionStrategy: position, + height: "100vh", + width: "100vw", + }, + ); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts index 886e1a966a8..0e57450fc77 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, OnDestroy } from "@angular/core"; +import { Component } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -13,7 +13,7 @@ import { VaultPopupListFiltersService } from "../../../services/vault-popup-list templateUrl: "./vault-list-filters.component.html", imports: [CommonModule, JslibModule, ChipSelectComponent, ReactiveFormsModule], }) -export class VaultListFiltersComponent implements OnDestroy { +export class VaultListFiltersComponent { protected filterForm = this.vaultPopupListFiltersService.filterForm; protected organizations$ = this.vaultPopupListFiltersService.organizations$; protected collections$ = this.vaultPopupListFiltersService.collections$; @@ -21,8 +21,4 @@ export class VaultListFiltersComponent implements OnDestroy { protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes; constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {} - - ngOnDestroy(): void { - this.vaultPopupListFiltersService.resetFilterForm(); - } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index f6e815dd461..6ac793e4d4d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -32,6 +32,11 @@ [size]="'small'" [appA11yTitle]="orgIconTooltip(cipher)" > + {{ cipher.subTitle }} 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 new file mode 100644 index 00000000000..15693ff18d0 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -0,0 +1,110 @@ +import { ComponentFixture, fakeAsync, flush, TestBed } from "@angular/core/testing"; +import { ActivatedRoute, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { Subject } from "rxjs"; + +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; + +import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; + +import { ViewV2Component } from "./view-v2.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("ViewV2Component", () => { + let component: ViewV2Component; + let fixture: ComponentFixture; + const params$ = new Subject(); + const mockNavigate = jest.fn(); + + const mockCipher = { + id: "122-333-444", + type: CipherType.Login, + }; + + const mockCipherService = { + get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), + getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), + }; + + beforeEach(async () => { + mockNavigate.mockClear(); + + await TestBed.configureTestingModule({ + imports: [ViewV2Component], + providers: [ + { provide: Router, useValue: { navigate: mockNavigate } }, + { provide: CipherService, useValue: mockCipherService }, + { provide: LogService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: mock() }, + { provide: ActivatedRoute, useValue: { queryParams: params$ } }, + { + provide: I18nService, + useValue: { + t: (key: string, ...rest: string[]) => { + if (rest?.length) { + return `${key} ${rest.join(" ")}`; + } + return key; + }, + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("queryParams", () => { + it("loads an existing cipher", fakeAsync(() => { + params$.next({ cipherId: "122-333-444" }); + + flush(); // Resolve all promises + + expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444"); + expect(component.cipher).toEqual(mockCipher); + })); + + it("sets the correct header text", fakeAsync(() => { + // Set header text for a login + mockCipher.type = CipherType.Login; + params$.next({ cipherId: mockCipher.id }); + flush(); // Resolve all promises + + expect(component.headerText).toEqual("viewItemHeader typelogin"); + + // Set header text for a card + mockCipher.type = CipherType.Card; + params$.next({ cipherId: mockCipher.id }); + flush(); // Resolve all promises + + expect(component.headerText).toEqual("viewItemHeader typecard"); + + // Set header text for an identity + mockCipher.type = CipherType.Identity; + params$.next({ cipherId: mockCipher.id }); + flush(); // Resolve all promises + + expect(component.headerText).toEqual("viewItemHeader typeidentity"); + + // Set header text for a secure note + mockCipher.type = CipherType.SecureNote; + params$.next({ cipherId: mockCipher.id }); + flush(); // Resolve all promises + + expect(component.headerText).toEqual("viewItemHeader note"); + })); + }); +}); 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 add2efed96a..ccc2658e59e 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 @@ -22,9 +22,11 @@ import { DialogService, ToastService, } from "@bitwarden/components"; +import { TotpCaptureService } from "@bitwarden/vault"; import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; +import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; @@ -34,6 +36,7 @@ import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup selector: "app-view-v2", templateUrl: "view-v2.component.html", standalone: true, + providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }], imports: [ CommonModule, SearchModule, @@ -51,7 +54,6 @@ import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup }) export class ViewV2Component { headerText: string; - cipherId: string; cipher: CipherView; organization$: Observable; folder$: Observable; @@ -72,14 +74,14 @@ export class ViewV2Component { subscribeToParams(): void { this.route.queryParams .pipe( - switchMap((param) => { - return this.getCipherData(param.cipherId); + switchMap(async (params): Promise => { + return await this.getCipherData(params.cipherId); }), takeUntilDestroyed(), ) - .subscribe((data) => { - this.cipher = data; - this.headerText = this.setHeader(data.type); + .subscribe((cipher) => { + this.cipher = cipher; + this.headerText = this.setHeader(cipher.type); }); } diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index 2bc74f79923..1a944d5599c 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -26,10 +26,10 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service"; -import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service"; import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service"; import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data"; import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window"; diff --git a/apps/browser/src/vault/popup/components/vault/vault-select.component.ts b/apps/browser/src/vault/popup/components/vault/vault-select.component.ts index de6a33724d1..6780cd57929 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-select.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-select.component.ts @@ -5,17 +5,28 @@ import { Component, ElementRef, EventEmitter, + HostListener, + OnDestroy, OnInit, Output, TemplateRef, ViewChild, ViewContainerRef, - HostListener, - OnDestroy, } from "@angular/core"; -import { BehaviorSubject, concatMap, map, merge, Observable, Subject, takeUntil } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + concatMap, + map, + merge, + Observable, + Subject, + takeUntil, +} from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -88,6 +99,7 @@ export class VaultSelectComponent implements OnInit, OnDestroy { private viewContainerRef: ViewContainerRef, private platformUtilsService: PlatformUtilsService, private organizationService: OrganizationService, + private policyService: PolicyService, ) {} @HostListener("document:keydown.escape", ["$event"]) @@ -103,11 +115,13 @@ export class VaultSelectComponent implements OnInit, OnDestroy { .pipe(takeUntil(this._destroy)) .pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name")))); - this.organizations$ + combineLatest([ + this.organizations$, + this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + ]) .pipe( - concatMap(async (organizations) => { - this.enforcePersonalOwnership = - await this.vaultFilterService.checkForPersonalOwnershipPolicy(); + concatMap(async ([organizations, enforcePersonalOwnership]) => { + this.enforcePersonalOwnership = enforcePersonalOwnership; if (this.shouldShow(organizations)) { if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) { diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts index 11331277a9c..a2b778984d7 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -2,14 +2,16 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; -import { combineLatest, map, Observable, shareReplay } from "rxjs"; +import { combineLatest, Observable, shareReplay, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; +import { BrowserApi } from "../../../../platform/browser/browser-api"; 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"; @@ -61,20 +63,24 @@ export class VaultV2Component implements OnInit, OnDestroy { protected newItemItemValues$: Observable = this.vaultPopupListFiltersService.filters$.pipe( - map((filter) => ({ - organizationId: (filter.organization?.id || - filter.collection?.organizationId) as OrganizationId, - collectionId: filter.collection?.id as CollectionId, - folderId: filter.folder?.id, - })), + switchMap( + async (filter) => + ({ + organizationId: (filter.organization?.id || + filter.collection?.organizationId) as OrganizationId, + collectionId: filter.collection?.id as CollectionId, + folderId: filter.folder?.id, + uri: (await BrowserApi.getTabFromCurrentWindow())?.url, + }) as NewItemInitialValues, + ), shareReplay({ refCount: true, bufferSize: 1 }), ); /** Visual state of the vault */ protected vaultState: VaultState | null = null; - protected vaultIcon = Icons.Vault; - protected deactivatedIcon = Icons.DeactivatedOrg; + protected vaultIcon = VaultIcons.Vault; + protected deactivatedIcon = VaultIcons.DeactivatedOrg; protected noResultsIcon = Icons.NoResults; protected VaultStateEnum = VaultState; diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index e48c0adc0c6..f3d95d3d203 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -27,10 +27,10 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service"; import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service"; import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data"; import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window"; diff --git a/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts b/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts new file mode 100644 index 00000000000..70993482046 --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-cipher-form-generation.service.ts @@ -0,0 +1,42 @@ +import { Overlay } from "@angular/cdk/overlay"; +import { inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; +import { CipherFormGenerationService } from "@bitwarden/vault"; + +import { VaultGeneratorDialogComponent } from "../components/vault-v2/vault-generator-dialog/vault-generator-dialog.component"; + +@Injectable() +export class BrowserCipherFormGenerationService implements CipherFormGenerationService { + private dialogService = inject(DialogService); + private overlay = inject(Overlay); + + async generatePassword(): Promise { + const dialogRef = VaultGeneratorDialogComponent.open(this.dialogService, this.overlay, { + data: { type: "password" }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result == null || result.action === "canceled") { + return null; + } + + return result.generatedValue; + } + + async generateUsername(): Promise { + const dialogRef = VaultGeneratorDialogComponent.open(this.dialogService, this.overlay, { + data: { type: "username" }, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result == null || result.action === "canceled") { + return null; + } + + return result.generatedValue; + } +} diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts index 2c9afacffd7..e790735dc52 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts @@ -13,10 +13,15 @@ describe("BrowserTotpCaptureService", () => { let testBed: TestBed; let service: BrowserTotpCaptureService; let mockCaptureVisibleTab: jest.SpyInstance; + let createNewTabSpy: jest.SpyInstance; const validTotpUrl = "otpauth://totp/label?secret=123"; beforeEach(() => { + const tabReturn = new Promise((resolve) => + resolve({ url: "google.com", active: true } as chrome.tabs.Tab), + ); + createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn); mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab"); mockCaptureVisibleTab.mockResolvedValue("screenshot"); @@ -66,4 +71,10 @@ describe("BrowserTotpCaptureService", () => { expect(result).toBeNull(); }); + + it("should call BrowserApi.createNewTab with a given loginURI", async () => { + await service.openAutofillNewTab("www.google.com"); + + expect(createNewTabSpy).toHaveBeenCalledWith("www.google.com"); + }); }); diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts index 3f8ba61ed36..8f93db45c0e 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -20,4 +20,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService { } return null; } + + async openAutofillNewTab(loginUri: string) { + await BrowserApi.createNewTab(loginUri); + } } diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html new file mode 100644 index 00000000000..565699a6f5b --- /dev/null +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html @@ -0,0 +1,32 @@ + + + + + + + +
    + + + {{ "theme" | i18n }} + + + + + + + + {{ "showNumberOfAutofillSuggestions" | i18n }} + + + + + {{ "enableFavicon" | i18n }} + + +
    +
    diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts new file mode 100644 index 00000000000..69186359e2b --- /dev/null +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts @@ -0,0 +1,110 @@ +import { Component, Input } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.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 { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +import { AppearanceV2Component } from "./appearance-v2.component"; + +@Component({ + standalone: true, + selector: "popup-header", + template: ``, +}) +class MockPopupHeaderComponent { + @Input() pageTitle: string; + @Input() backAction: () => void; +} + +@Component({ + standalone: true, + selector: "popup-page", + template: ``, +}) +class MockPopupPageComponent {} + +describe("AppearanceV2Component", () => { + let component: AppearanceV2Component; + let fixture: ComponentFixture; + + const showFavicons$ = new BehaviorSubject(true); + const enableBadgeCounter$ = new BehaviorSubject(true); + const selectedTheme$ = new BehaviorSubject(ThemeType.Nord); + const setSelectedTheme = jest.fn().mockResolvedValue(undefined); + const setShowFavicons = jest.fn().mockResolvedValue(undefined); + const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined); + + beforeEach(async () => { + setSelectedTheme.mockClear(); + setShowFavicons.mockClear(); + setEnableBadgeCounter.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AppearanceV2Component], + providers: [ + { provide: ConfigService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: MessagingService, useValue: mock() }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: DomainSettingsService, useValue: { showFavicons$, setShowFavicons } }, + { + provide: BadgeSettingsServiceAbstraction, + useValue: { enableBadgeCounter$, setEnableBadgeCounter }, + }, + { provide: ThemeStateService, useValue: { selectedTheme$, setSelectedTheme } }, + ], + }) + .overrideComponent(AppearanceV2Component, { + remove: { + imports: [PopupHeaderComponent, PopupPageComponent], + }, + add: { + imports: [MockPopupHeaderComponent, MockPopupPageComponent], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AppearanceV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("populates the form with the user's current settings", () => { + expect(component.appearanceForm.value).toEqual({ + enableFavicon: true, + enableBadgeCounter: true, + theme: ThemeType.Nord, + }); + }); + + describe("form changes", () => { + it("updates the users theme", () => { + component.appearanceForm.controls.theme.setValue(ThemeType.Light); + + expect(setSelectedTheme).toHaveBeenCalledWith(ThemeType.Light); + }); + + it("updates the users favicon setting", () => { + component.appearanceForm.controls.enableFavicon.setValue(false); + + expect(setShowFavicons).toHaveBeenCalledWith(false); + }); + + it("updates the users badge counter setting", () => { + component.appearanceForm.controls.enableBadgeCounter.setValue(false); + + expect(setEnableBadgeCounter).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts new file mode 100644 index 00000000000..7ca073d51b0 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -0,0 +1,110 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { CheckboxModule } from "@bitwarden/components"; + +import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; +import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; +import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; +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"; + +@Component({ + standalone: true, + templateUrl: "./appearance-v2.component.html", + imports: [ + CommonModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + CardComponent, + FormFieldModule, + SelectModule, + ReactiveFormsModule, + CheckboxModule, + ], +}) +export class AppearanceV2Component implements OnInit { + appearanceForm = this.formBuilder.group({ + enableFavicon: false, + enableBadgeCounter: true, + theme: ThemeType.System, + }); + + /** Available theme options */ + themeOptions: { name: string; value: ThemeType }[]; + + constructor( + private messagingService: MessagingService, + private domainSettingsService: DomainSettingsService, + private badgeSettingsService: BadgeSettingsServiceAbstraction, + private themeStateService: ThemeStateService, + private formBuilder: FormBuilder, + private destroyRef: DestroyRef, + i18nService: I18nService, + ) { + this.themeOptions = [ + { name: i18nService.t("systemDefault"), value: ThemeType.System }, + { name: i18nService.t("light"), value: ThemeType.Light }, + { name: i18nService.t("dark"), value: ThemeType.Dark }, + { name: "Nord", value: ThemeType.Nord }, + { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, + ]; + } + + async ngOnInit() { + const enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); + const enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); + const theme = await firstValueFrom(this.themeStateService.selectedTheme$); + + // Set initial values for the form + this.appearanceForm.setValue({ + enableFavicon, + enableBadgeCounter, + theme, + }); + + this.appearanceForm.controls.theme.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((newTheme) => { + void this.saveTheme(newTheme); + }); + + this.appearanceForm.controls.enableFavicon.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((enableFavicon) => { + void this.updateFavicon(enableFavicon); + }); + + this.appearanceForm.controls.enableBadgeCounter.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((enableBadgeCounter) => { + void this.updateBadgeCounter(enableBadgeCounter); + }); + } + + async updateFavicon(enableFavicon: boolean) { + await this.domainSettingsService.setShowFavicons(enableFavicon); + } + + async updateBadgeCounter(enableBadgeCounter: boolean) { + await this.badgeSettingsService.setEnableBadgeCounter(enableBadgeCounter); + this.messagingService.send("bgUpdateContextMenu"); + } + + async saveTheme(newTheme: ThemeType) { + await this.themeStateService.setSelectedTheme(newTheme); + } +} diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders-v2.component.html new file mode 100644 index 00000000000..21e00757a29 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + {{ folder.name }} + + + + + + + + + {{ "noFoldersAdded" | i18n }} + {{ "createFoldersToOrganize" | i18n }} + + + + + diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts new file mode 100644 index 00000000000..eecad04613e --- /dev/null +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts @@ -0,0 +1,115 @@ +import { Component, Input } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { DialogService } from "@bitwarden/components"; + +import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { AddEditFolderDialogComponent } from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component"; + +import { FoldersV2Component } from "./folders-v2.component"; + +@Component({ + standalone: true, + selector: "popup-header", + template: ``, +}) +class MockPopupHeaderComponent { + @Input() pageTitle: string; + @Input() backAction: () => void; +} + +@Component({ + standalone: true, + selector: "popup-footer", + template: ``, +}) +class MockPopupFooterComponent { + @Input() pageTitle: string; +} + +describe("FoldersV2Component", () => { + let component: FoldersV2Component; + let fixture: ComponentFixture; + const folderViews$ = new BehaviorSubject([]); + const open = jest.fn(); + + beforeEach(async () => { + open.mockClear(); + + await TestBed.configureTestingModule({ + imports: [FoldersV2Component], + providers: [ + { provide: PlatformUtilsService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: LogService, useValue: mock() }, + { provide: FolderService, useValue: { folderViews$ } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }) + .overrideComponent(FoldersV2Component, { + remove: { + imports: [PopupHeaderComponent, PopupFooterComponent], + }, + add: { + imports: [MockPopupHeaderComponent, MockPopupFooterComponent], + }, + }) + .overrideProvider(DialogService, { useValue: { open } }) + .compileComponents(); + + fixture = TestBed.createComponent(FoldersV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(() => { + folderViews$.next([ + { id: "1", name: "Folder 1" }, + { id: "2", name: "Folder 2" }, + { id: "0", name: "No Folder" }, + ] as FolderView[]); + fixture.detectChanges(); + }); + + it("removes the last option in the folder array", (done) => { + component.folders$.subscribe((folders) => { + expect(folders).toEqual([ + { id: "1", name: "Folder 1" }, + { id: "2", name: "Folder 2" }, + ]); + done(); + }); + }); + + it("opens edit dialog for existing folder", () => { + const folder = { id: "1", name: "Folder 1" } as FolderView; + const editButton = fixture.debugElement.query(By.css('[data-testid="edit-folder-button"]')); + + editButton.triggerEventHandler("click"); + + expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { + data: { editFolderConfig: { folder } }, + }); + }); + + it("opens add dialog for new folder when there are no folders", () => { + folderViews$.next([]); + fixture.detectChanges(); + + const addButton = fixture.debugElement.query(By.css('[data-testid="empty-new-folder-button"]')); + + addButton.triggerEventHandler("click"); + + expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} }); + }); +}); diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts new file mode 100644 index 00000000000..ce196132f88 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -0,0 +1,74 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + AsyncActionsModule, + ButtonModule, + DialogService, + IconButtonModule, +} from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; + +import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component"; +import { ItemModule } from "../../../../../../libs/components/src/item/item.module"; +import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module"; +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 { + AddEditFolderDialogComponent, + AddEditFolderDialogData, +} from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component"; + +@Component({ + standalone: true, + templateUrl: "./folders-v2.component.html", + imports: [ + CommonModule, + JslibModule, + PopOutComponent, + PopupPageComponent, + PopupHeaderComponent, + ItemModule, + ItemGroupComponent, + NoItemsModule, + IconButtonModule, + ButtonModule, + AsyncActionsModule, + ], +}) +export class FoldersV2Component { + folders$: Observable; + + NoFoldersIcon = VaultIcons.NoFolders; + + constructor( + private folderService: FolderService, + private dialogService: DialogService, + ) { + this.folders$ = this.folderService.folderViews$.pipe( + map((folders) => { + // Remove the last folder, which is the "no folder" option folder + if (folders.length > 0) { + return folders.slice(0, folders.length - 1); + } + + return folders; + }), + ); + } + + /** Open the Add/Edit folder dialog */ + openAddEditFolderDialog(folder?: FolderView) { + // If a folder is provided, the edit variant should be shown + const editFolderConfig = folder ? { folder } : undefined; + + this.dialogService.open(AddEditFolderDialogComponent, { + data: { editFolderConfig }, + }); + } +} diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index e864e27ed0b..ea98321a499 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -170,7 +170,7 @@ Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 - 从多台设备同步和访问密码库 + 在多个设备间同步和访问您的密码库 在一个安全的密码库中管理您所有的登录信息和密码 diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 7f10b4075f5..4bb440315a1 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -148,6 +148,21 @@ const webNavigation = { addListener: jest.fn(), removeListener: jest.fn(), }, + onCompleted: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + +const webRequest = { + onBeforeRequest: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, + onBeforeRedirect: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, }; const alarms = { @@ -177,5 +192,6 @@ global.chrome = { offscreen, permissions, webNavigation, + webRequest, alarms, } as any; diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 36007f26f0c..04c4f554f38 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -179,6 +179,7 @@ const mainConfig = { "content/bootstrap-legacy-autofill-overlay": "./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts", "content/autofiller": "./src/autofill/content/autofiller.ts", + "content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts", "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", "content/content-message-handler": "./src/autofill/content/content-message-handler.ts", diff --git a/apps/cli/package.json b/apps/cli/package.json index 5f417579081..cb0c2a01678 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.2", + "version": "2024.8.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index d767ee80b37..f4486ff9667 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -69,7 +69,7 @@ export class UnlockCommand { } const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey); + await this.cryptoService.setUserKey(userKey, userId); if (await this.keyConnectorService.getConvertAccountRequired()) { const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand( diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 61d4607cea0..977a00b04dd 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.2", + "version": "2024.8.0", "keywords": [ "bitwarden", "password", @@ -52,7 +52,7 @@ "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.certificateSubjectName=\"8bit Solutions LLC\"", "publish:win:dev": "npm run build && 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)\" --username $APPLE_ID_USERNAME --password $APPLE_ID_PASSWORD", + "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", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js index 97815bc8b9b..69c078a13b5 100644 --- a/apps/desktop/scripts/after-sign.js +++ b/apps/desktop/scripts/after-sign.js @@ -50,14 +50,27 @@ async function run(context) { if (macBuild) { console.log("### Notarizing " + appPath); - const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID; - const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`; - return await notarize({ - tool: "notarytool", - appPath: appPath, - teamId: "LTZ2PFU5D6", - appleId: appleId, - appleIdPassword: appleIdPassword, - }); + if (process.env.APP_STORE_CONNECT_TEAM_ISSUER) { + const appleApiIssuer = process.env.APP_STORE_CONNECT_TEAM_ISSUER; + const appleApiKey = process.env.APP_STORE_CONNECT_AUTH_KEY_PATH; + const appleApiKeyId = process.env.APP_STORE_CONNECT_AUTH_KEY; + return await notarize({ + tool: "notarytool", + appPath: appPath, + appleApiIssuer: appleApiIssuer, + appleApiKey: appleApiKey, + appleApiKeyId: appleApiKeyId, + }); + } else { + const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID; + const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`; + return await notarize({ + tool: "notarytool", + appPath: appPath, + teamId: "LTZ2PFU5D6", + appleId: appleId, + appleIdPassword: appleIdPassword, + }); + } } } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index f51b07b0a7f..84b45245c42 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Bykomende Windows Hello-instellings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bevestig vir Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Ontgrendel met Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Vra vir Windows Hello by lansering" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Vra vir Touch ID by lansering" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Vir blaaierbiometrie moet werkskermbiometrie eers in instellings geaktiveer wees." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Weens ’n ondernemingsbeleid mag u geen wagwoorde in u persoonlike kluis bewaar nie. Verander die eienaarskap na ’n organisasie en kies uit ’n van die beskikbare versamelings." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "U hoofwagwoord voldoen nie aan een of meer van die organisasiebeleide nie. Om toegang tot die kluis te kry, moet u nou u hoofwagwoord bywerk. Deur voort te gaan sal u van u huidige sessie afgeteken word, en u sal weer moet aanteken. Aktiewe sessies op ander toestelle kan vir tot een uur aktief bly." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 3b6957fccdb..27cb1e1ba1f 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "إعدادات Windows Hello إضافية" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "تحقق من Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "فتح بواسطة معرف اللمس" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "اسأل عن Windows Hello عند التشغيل" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "اطلب معرف اللمس عند التشغيل" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "القياسات الحيوية للمتصفح تتطلب القياسات الحيوية لسطح المكتب ليتم تمكينها في الإعدادات أولاً." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "بسبب سياسة المؤسسة، يمنع عليك حفظ العناصر في خزانتك الشخصية. غيّر خيار الملكية إلى مؤسسة واختر من المجموعات المتاحة." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "كلمة المرور الرئيسية الخاصة بك لا تفي بواحدة أو أكثر من سياسات مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديث كلمة المرور الرئيسية الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. وقد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "حاول مرة أخرى" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 6e8967e5a35..923b1c54006 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Əlavə Windows Hello ayarları" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bitwarden üçün doğrula." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Touch ID kilidini aç" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Açılışda Windows Hello-nu soruşun" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Açılışda Touch ID-ni soruşun" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Brauzer biometrikləri, əvvəlcə ayarlarda masaüstü biometriklərinin qurulmasını tələb edir." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Anbara müraciət üçü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." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Anbarınıza müraciət etmək üçün lütfən ana parol təyin edin." + }, "tryAgain": { "message": "Yenidən sına" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index e3c4f8f97ce..9931724dd9b 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Дадатковыя налады Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Праверыць на Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Разблакіраваць з Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Пытацца пра Windows Hello пры запуску" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Пытацца пра Touch ID пры запуску" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Для актывацыі біяметрыі ў браўзеры неабходна спачатку ўключыць яе ў наладах праграмы для камп'ютара." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш асноўны пароль не адпавядае адной або некалькім палітыкам арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць яго. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Актыўныя сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index e6db6903e22..ce84d0259ef 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Допълнителни настройки на Windows Hello" }, + "unlockWithPolkit": { + "message": "Отключване чрез системно удостоверяване" + }, "windowsHelloConsentMessage": { "message": "Потвърждаване за Битуорден." }, + "polkitConsentMessage": { + "message": "Идентифицирайте се, за да отключите Битуорден." + }, "unlockWithTouchId": { "message": "Отключване с Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Питане за Windows Hello при пускане" }, + "autoPromptPolkit": { + "message": "Питане за системно удостоверяване при стартиране" + }, "autoPromptTouchId": { "message": "Питане за Touch ID при пускане" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Потвърждаването с биометрични данни в браузъра изисква включването включването им в настройките за самостоятелното приложение." }, + "biometricsManualSetupTitle": { + "message": "Автоматичното настройване не е налично" + }, + "biometricsManualSetupDesc": { + "message": "Поради начина на инсталиране, поддръжката на биометрични данни не може да бъде включена автоматично. Искате ли да отворите документацията, за да видите как да го направите ръчно?" + }, "personalOwnershipSubmitError": { "message": "Заради някоя политика за голяма организация не може да запазвате елементи в собствения си трезор. Променете собствеността да е на организация и изберете от наличните колекции." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Вашата организация е деактивирала шифроването чрез доверени устройства. Задайте главна парола, за да получите достъп до трезора си." + }, "tryAgain": { "message": "Нов опит" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Данни" + }, + "fileSends": { + "message": "Файлови изпращания" + }, + "textSends": { + "message": "Текстови изпращания" } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 83437ea4573..70b45364cc8 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "ব্রাউজার বায়োমেট্রিক্সের জন্য প্রথমে সেটিংসে ডেস্কটপ বায়োমেট্রিক সক্ষম করা প্রয়োজন।" }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "একটি এন্টারপ্রাইজ নীতির কারণে, আপনি আপনার ব্যক্তিগত ভল্টে বস্তুসমূহ সংরক্ষণ করা থেকে সীমাবদ্ধ। একটি প্রতিষ্ঠানের মালিকানা বিকল্পটি পরিবর্তন করুন এবং উপলভ্য সংগ্রহগুলি থেকে চয়ন করুন।" }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 939fee08c76..1e5d34decb7 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Potvrdi za Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Otključaj koristeći Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrija preglednika zahtijeva prethodno omogućenu biometriju u Bitwarden desktop aplikaciji." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Zbog poslovnih smjernica, zabranjeno vam je pohranjivanje predmeta u svoj lični trezor. Promijenite opciju vlasništva u organizaciji i odaberite neku od dostupnih kolekcija." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 61744522b71..8827bcd7355 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -500,10 +500,10 @@ "message": "Crea un compte" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Estableix una contrasenya segura" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Acabeu de crear el vostre compte establint una contrasenya" }, "logIn": { "message": "Inicia sessió" @@ -527,7 +527,7 @@ "message": "Pista de la contrasenya mestra (opcional)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Si oblideu la contrasenya, la pista de contrasenya es pot enviar al vostre correu electrònic. $CURRENT$/$MAXIMUM$ caràcters màxim.", "placeholders": { "current": { "content": "$1", @@ -540,22 +540,22 @@ } }, "masterPassword": { - "message": "Master password" + "message": "Contrasenya mestra" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "La contrasenya mestra no es pot recuperar si la oblideu!" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Confirma la contrasenya mestra" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Pista de la contrasenya mestra" }, "joinOrganization": { - "message": "Join organization" + "message": "Uneix-te a l'organització" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Acabeu d'unir-vos a aquesta organització establint una contrasenya mestra." }, "settings": { "message": "Configuració" @@ -634,7 +634,7 @@ "message": "El codi de verificació és obligatori." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "L'autenticació s'ha cancel·lat o ha tardat massa. Torna-ho a provar." }, "invalidVerificationCode": { "message": "Codi de verificació no vàlid" @@ -692,7 +692,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Clau de seguretat OTP de Yubico" }, "yubiKeyDesc": { "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." @@ -739,7 +739,7 @@ "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Per a la configuració avançada, podeu especificar l'URL base de cada servei de manera independent." }, "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." @@ -802,7 +802,7 @@ "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Enllaç caducat" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -1370,7 +1370,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Exporta des de" }, "exportVault": { "message": "Exporta caixa forta" @@ -1382,7 +1382,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Contrasenya del fitxer" }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" @@ -1391,13 +1391,13 @@ "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." }, "passwordProtected": { - "message": "Password protected" + "message": "Protegit amb contrasenya" }, "passwordProtectedOptionDescription": { "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Tipus d'exportació" }, "accountRestricted": { "message": "Account restricted" @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Configuració addicional de Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verifica per Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Desbloqueja amb Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Sol·liciteu Windows Hello en iniciar" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Sol·liciteu Touch ID en iniciar" }, @@ -1715,16 +1724,16 @@ "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Anul·la la subscripció" }, "atAnyTime": { - "message": "at any time." + "message": "en qualsevol moment." }, "byContinuingYouAgreeToThe": { "message": "By continuing, you agree to the" }, "and": { - "message": "and" + "message": "i" }, "acceptPolicies": { "message": "Si activeu aquesta casella, indiqueu que esteu d’acord amb el següent:" @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "La biometria del navegador primer necessita habilitar la biomètrica d’escriptori a la configuració." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "A causa d'una política empresarial, no podeu guardar elements a la vostra caixa forta personal. Canvieu l'opció Propietat en organització i trieu entre les col·leccions disponibles." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "La vostra contrasenya mestra no compleix una o més de les polítiques de l'organització. Per accedir a la caixa forta, heu d'actualitzar-la ara. Si continueu, es tancarà la sessió actual i us demanarà que torneu a iniciar-la. Les sessions en altres dispositius poden continuar romanent actives fins a una hora." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Torneu-ho a provar" }, @@ -2521,13 +2539,13 @@ "message": "S'ha sol·licitat inici de sessió" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Creant compte en" }, "checkYourEmail": { - "message": "Check your email" + "message": "Comprova el teu correu" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Seguiu l'enllaç del correu electrònic enviat a" }, "andContinueCreatingYourAccount": { "message": "and continue creating your account." @@ -2536,7 +2554,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Torna arrere" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2827,7 +2845,7 @@ "message": "La contrasenya del fitxer no és vàlida. Utilitzeu la contrasenya que vau introduir quan vau crear el fitxer d'exportació." }, "destination": { - "message": "Destination" + "message": "Destinació" }, "learnAboutImportOptions": { "message": "Obteniu informació sobre les opcions d'importació" @@ -2886,7 +2904,7 @@ "message": "Confirma la contrasenya del fitxer" }, "exportSuccess": { - "message": "Vault data exported" + "message": "S'han exportat les dades de la caixa forta" }, "multifactorAuthenticationCancelled": { "message": "S'ha cancel·lat l'autenticació multifactor" @@ -3010,7 +3028,7 @@ } }, "back": { - "message": "Back", + "message": "Arrere", "description": "Button text to navigate back" }, "removeItem": { @@ -3024,6 +3042,12 @@ } }, "data": { - "message": "Data" + "message": "Dades" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index d28b464fdd4..2ab8dfa806c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Další nastavení Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Ověřte se pro Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Odemknout pomocí Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Požádat o Windows Hello při spuštění aplikace" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Požádat o Touch ID při spuštění aplikace" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrické prvky v prohlížeči vyžadují, aby byla nastavena biometrie nejprve v aplikaci pro počítač." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Z důvodu podnikových zásad nemůžete ukládat položky do svého osobního trezoru. Změňte vlastnictví položky na organizaci a poté si vyberte z dostupných kolekcí." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Vaše hlavní heslo nesplňuje jednu nebo více zásad Vaší organizace. Pro přístup k trezoru musíte nyní aktualizovat své hlavní heslo. Pokračování Vás odhlásí z Vaší aktuální relace a bude nutné se přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Vaše organizace zakázala šifrování pomocí důvěryhodného zařízení. Nastavte hlavní heslo pro přístup k trezoru." + }, "tryAgain": { "message": "Zkusit znovu" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 3cb285cf21c..19ef9c8ffbb 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index b99ec3b53b0..ad371a4d116 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Yderligere indstillinger for Windows Hello" }, + "unlockWithPolkit": { + "message": "Oplås med systemgodkendelse" + }, "windowsHelloConsentMessage": { "message": "Bekræft for Bitwarden." }, + "polkitConsentMessage": { + "message": "Godkend for at oplåse Bitwarden." + }, "unlockWithTouchId": { "message": "Oplås med Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Anmod om Windows Hello ved app-start" }, + "autoPromptPolkit": { + "message": "Anmod om systemgodkendelse ved start" + }, "autoPromptTouchId": { "message": "Anmod om Touch ID ved app-start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browserbiometri kræver, at computerbiometri er opsat i indstillingerne først." }, + "biometricsManualSetupTitle": { + "message": "Automatisk opsætning utilgængelig" + }, + "biometricsManualSetupDesc": { + "message": "Grundet installationsmetoden kunne biometriunderstøttelse ikke automatisk aktiveres. Åbn dokumentationen til, hvordan dette gøres manuelt?" + }, "personalOwnershipSubmitError": { "message": "Grundet en virksomhedspolitik forhindres du i at gemme emner i din personlige boks. Skift ejerskabsindstillingen til en organisation, og vælg blandt de tilgængelige samlinger." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Din hovedadgangskode overholder ikke en eller flere organisationspolitikker. For at få adgang til boksen skal hovedadgangskode opdateres nu. Fortsættes, logges du ud af den nuværende session og vil skulle logger ind igen. Aktive sessioner på andre enheder kan forblive aktive i op til én time." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Organisationen har deaktiveret betroet enhedskryptering. Opsæt en hovedadgangskode for at tilgå boksen." + }, "tryAgain": { "message": "Forsøg igen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "Fil-Sends" + }, + "textSends": { + "message": "Tekst-Sends" } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 47f07af7c33..6e5d5644c8f 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Zusätzliche Einstellungen für Windows Hello" }, + "unlockWithPolkit": { + "message": "Mit Systemauthentifizierung entsperren" + }, "windowsHelloConsentMessage": { "message": "Für Bitwarden verifizieren." }, + "polkitConsentMessage": { + "message": "Authentifizieren, um Bitwarden zu entsperren." + }, "unlockWithTouchId": { "message": "Mit Touch ID entsperren" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Beim Start nach Windows Hello fragen" }, + "autoPromptPolkit": { + "message": "Beim Start nach Systemauthentifizierung fragen" + }, "autoPromptTouchId": { "message": "Beim Start nach Touch ID fragen" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrie im Browser setzt voraus, dass Biometrie zuerst in den Einstellungen der Desktop-Anwendung eingerichtet wird." }, + "biometricsManualSetupTitle": { + "message": "Automatische Einrichtung nicht verfügbar" + }, + "biometricsManualSetupDesc": { + "message": "Aufgrund der Installationsmethode konnte die Biometrie-Unterstützung nicht automatisch aktiviert werden. Möchtest du die Dokumentation für die manuelle Aktivierung öffnen?" + }, "personalOwnershipSubmitError": { "message": "Aufgrund einer Unternehmensrichtlinie darfst du keine Einträge in deinem persönlichen Tresor speichern. Ändere die Eigentümer-Option in eine Organisation und wähle aus den verfügbaren Sammlungen." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Dein Master-Passwort entspricht nicht einer oder mehreren Richtlinien deiner Organisation. Um auf den Tresor zugreifen zu können, musst du dein Master-Passwort jetzt aktualisieren. Wenn du fortfährst, wirst du von deiner aktuellen Sitzung abgemeldet und musst dich erneut anmelden. Aktive Sitzungen auf anderen Geräten können noch bis zu einer Stunde lang aktiv bleiben." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Deine Organisation hat die vertrauenswürdige Geräteverschlüsselung deaktiviert. Bitte lege ein Master-Passwort fest, um auf deinen Tresor zuzugreifen." + }, "tryAgain": { "message": "Erneut versuchen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Daten" + }, + "fileSends": { + "message": "Datei-Sends" + }, + "textSends": { + "message": "Text-Sends" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index a4b7e5e64a6..2026cfa9720 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -33,7 +33,7 @@ "message": "Συλλογές" }, "searchVault": { - "message": "Αναζήτηση στο θησαυ/κιο" + "message": "Αναζήτηση κρύπτης" }, "addItem": { "message": "Προσθήκη αντικειμένου" @@ -257,7 +257,7 @@ "message": "Άλλες" }, "generatePassword": { - "message": "Δημιουργία κωδικού πρόσβασης" + "message": "Γέννηση κωδικού πρόσβασης" }, "type": { "message": "Τύπος" @@ -364,7 +364,7 @@ "message": "Διαγραφή συνημμένου" }, "deleteItemConfirmation": { - "message": "Είστε βέβαιοι ότι θέλετε να το μετακινήσετε στον κάδο;" + "message": "Σίγουρα θέλετε να το μετακινήσετε στον κάδο;" }, "deletedItem": { "message": "Το αντικείμενο μετακινήθηκε στον κάδο απορριμάτων" @@ -479,7 +479,7 @@ "message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB." }, "encryptionKeyMigrationRequired": { - "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω του διαδικτυακού θησαυ/κίου για να ενημερώσετε το κλειδί κρυπτογράφησης σας." + "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω της διαδικτυακής κρύπτης για να ενημερώσετε το κλειδί κρυπτογράφησης σας." }, "editedFolder": { "message": "Ο φάκελος αποθηκεύτηκε" @@ -488,7 +488,7 @@ "message": "Ο φάκελος προστέθηκε" }, "deleteFolderConfirmation": { - "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον φάκελο;" + "message": "Σίγουρα θέλετε να διαγράψετε αυτόν τον φάκελο;" }, "deletedFolder": { "message": "Ο φάκελος διαγράφηκε" @@ -552,7 +552,7 @@ "message": "Υπόδειξη κύριου κωδικού πρόσβασης" }, "joinOrganization": { - "message": "Συμμετοχή στον οργανισμό" + "message": "Συμμετοχή σε οργανισμό" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Ολοκληρώστε τη συμμετοχή σας σε αυτόν τον οργανισμό ορίζοντας έναν κύριο κωδικό πρόσβασης." @@ -757,7 +757,7 @@ "message": "URL διακομιστή API" }, "webVaultUrl": { - "message": "URL διακομιστή διαδικτυακού θησαυ/κίου" + "message": "URL διακομιστή διαδικτυακής κρύπτης" }, "identityUrl": { "message": "URL διακομιστή ταυτότητας" @@ -805,7 +805,7 @@ "message": "Ο σύνδεσμος έληξε" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Παρακαλούμε επανακκινήστε την εγγραφή ή δοκιμάστε να συνδεθείτε." + "message": "Παρακαλούμε επανεκκινήστε την εγγραφή ή δοκιμάστε να συνδεθείτε." }, "youMayAlreadyHaveAnAccount": { "message": "Μπορεί να έχετε ήδη λογαριασμό" @@ -835,7 +835,7 @@ "message": "Φόρτωση..." }, "lockVault": { - "message": "Κλείδωμα θησαυ/κίου" + "message": "Κλείδωμα κρύπτης" }, "passwordGenerator": { "message": "Γεννήτρια κωδικού πρόσβασης" @@ -859,7 +859,7 @@ "message": "Ακολουθήστε μας" }, "syncVault": { - "message": "Συγχρονισμός θησαυ/κίου" + "message": "Συγχρονισμός κρύπτης" }, "changeMasterPass": { "message": "Αλλαγή κύριου κωδικού πρόσβασης" @@ -879,13 +879,13 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "goToWebVault": { - "message": "Πηγαίνετε στο διαδικτυακό θησαυ/κιο" + "message": "Πηγαίνετε στη διαδικτυακή κρύπτη" }, "getMobileApp": { - "message": "Λήψη εφαρμογής για το κινητό" + "message": "Απόκτηση εφαρμογής για το κινητό" }, "getBrowserExtension": { - "message": "Λήψη επέκτασης περιηγητή" + "message": "Απόκτηση επέκτασης περιηγητή" }, "syncingComplete": { "message": "Ο συγχρονισμός ολοκληρώθηκε" @@ -894,7 +894,7 @@ "message": "Ο συγχρονισμός απέτυχε" }, "yourVaultIsLocked": { - "message": "Το θησαυ/κιό σας είναι κλειδωμένο. Επαληθεύστε την ταυτότητά σας για να συνεχίσετε." + "message": "Η κρύπτη σας είναι κλειδωμένη. Επαληθεύστε την ταυτότητά σας για να συνεχίσετε." }, "unlock": { "message": "Ξεκλείδωμα" @@ -916,16 +916,16 @@ "message": "Μη έγκυρος κύριος κωδικός πρόσβασης" }, "twoStepLoginConfirmation": { - "message": "Η σύνδεση δύο βημάτων καθιστά τον λογαριασμό σας πιο ασφαλή απαιτώντας από εσάς να επαληθεύσετε τη σύνδεσή σας με άλλη συσκευή, όπως ένα κλειδί ασφαλείας, μία εφαρμογή αυθεντικοποίησης, ένα SMS, μία τηλεφωνική κλήση, ή ένα μήνυμα ηλ. ταχυδρομείου. Η σύνδεση δύο βημάτων μπορεί να ρυθμιστεί στο διαδικτυακό θησαυ/κιο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "message": "Η σύνδεση δύο βημάτων καθιστά τον λογαριασμό σας πιο ασφαλή απαιτώντας από εσάς να επαληθεύσετε τη σύνδεσή σας με άλλη συσκευή, όπως ένα κλειδί ασφαλείας, μία εφαρμογή αυθεντικοποίησης, ένα SMS, μία τηλεφωνική κλήση, ή ένα μήνυμα ηλ. ταχυδρομείου. Η σύνδεση δύο βημάτων μπορεί να ρυθμιστεί στη διαδικτυακή κρύπτη bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, "twoStepLogin": { "message": "Σύνδεση δύο βημάτων" }, "vaultTimeout": { - "message": "Χρονικό όριο λήξης θησαυ/κίου" + "message": "Χρονικό όριο λήξης κρύπτης" }, "vaultTimeoutDesc": { - "message": "Επιλέξτε πότε το θησαυ/κιό σας θα αναλάβει τη δράση χρονικού ορίου λήξης θησαυ/κίου." + "message": "Επιλέξτε πότε η κρύπτη σας θα αναλάβει τη δράση χρονικού ορίου λήξης κρύπτης." }, "immediately": { "message": "Άμεσα" @@ -1280,7 +1280,7 @@ "message": "Σφάλμα Ανανέωσης Διακριτικού Πρόσβασης" }, "errorRefreshingAccessTokenDesc": { - "message": "Δεν βρέθηκε διακριτικό ανανέωσης ή κλειδιά API. Παρακαλούμε δοκιμάστε να αποσυνδεθείτε και να συνδεθείτε ξανά." + "message": "Δε βρέθηκε διακριτικό ανανέωσης ή κλειδιά API. Παρακαλούμε δοκιμάστε να αποσυνδεθείτε και να συνδεθείτε ξανά." }, "help": { "message": "Βοήθεια" @@ -1373,7 +1373,7 @@ "message": "Εξαγωγή από" }, "exportVault": { - "message": "Εξαγωγή θησαυ/κίου" + "message": "Εξαγωγή κρύπτης" }, "fileFormat": { "message": "Τύπος αρχείου" @@ -1403,7 +1403,7 @@ "message": "Ο λογαριασμός περιορίστηκε" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“Κωδικός πρόσβασης αρχείου” και “Επιβεβαίωση κωδικού πρόσβασης αρχείου“ δεν ταιριάζουν." + "message": "Το \"Κωδικός πρόσβασης αρχείου\" και το \"Επιβεβαίωση κωδικού πρόσβασης αρχείου\" δεν ταιριάζουν." }, "hCaptchaUrl": { "message": "hCaptcha Url", @@ -1444,7 +1444,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Επιβεβαίωση εξαγωγής θησαυ/κίου" + "message": "Επιβεβαίωση εξαγωγής κρύπτης" }, "exportWarningDesc": { "message": "Αυτή η εξαγωγή περιέχει τα δεδομένα σε μη κρυπτογραφημένη μορφή. Δεν πρέπει να αποθηκεύετε ή να στείλετε το εξαγόμενο αρχείο μέσω μη ασφαλών τρόπων (όπως μέσω email). Διαγράψτε το αμέσως μόλις τελειώσετε με τη χρήση του." @@ -1480,7 +1480,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Ασθενής κύριος κωδικός πρόσβασης" + "message": "Αδύναμος κύριος κωδικός πρόσβασης" }, "weakMasterPasswordDesc": { "message": "Ο κύριος κωδικός που έχετε επιλέξει είναι αδύναμος. Θα πρέπει να χρησιμοποιήσετε έναν ισχυρό κύριο κωδικό (ή μια φράση) για την κατάλληλη προστασία του λογαριασμού Bitwarden. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κύριο κωδικό;" @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Πρόσθετες ρυθμίσεις του Windows Hello" }, + "unlockWithPolkit": { + "message": "Ξεκλείδωμα με αυθεντικοποίηση συστήματος" + }, "windowsHelloConsentMessage": { "message": "Επαληθεύστε για το Bitwarden." }, + "polkitConsentMessage": { + "message": "Αυθεντικοποίηση για ξεκλείδωμα του Bitwarden." + }, "unlockWithTouchId": { "message": "Ξεκλείδωμα με Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Να ζητείται Windows Hello κατά την εκκίνηση της εφαρμογής" }, + "autoPromptPolkit": { + "message": "Ρώτησε με για αυθεντικοποίηση συστήματος κατά την εκκίνηση" + }, "autoPromptTouchId": { "message": "Ερώτηση για το Touch ID κατά την εκκίνηση" }, @@ -1541,7 +1550,7 @@ "message": "Διαγραφή λογαριασμού" }, "deleteAccountDesc": { - "message": "Προχωρήστε παρακάτω για να διαγράψετε τον λογαριασμό σας και όλα τα δεδομένα θησαυ/κίου." + "message": "Προχωρήστε παρακάτω για να διαγράψετε τον λογαριασμό σας και όλα τα δεδομένα κρύπτης." }, "deleteAccountWarning": { "message": "Η διαγραφή του λογαριασμού σας είναι μόνιμη. Δεν μπορεί να αναιρεθεί." @@ -1599,13 +1608,13 @@ "message": "Μία ή περισσότερες πολιτικές του οργανισμού επηρεάζουν τις ρυθμίσεις της γεννήτριας." }, "vaultTimeoutAction": { - "message": "Ενέργεια χρονικού ορίου λήξης θησαυ/κίου" + "message": "Ενέργεια χρονικού ορίου λήξης κρύπτης" }, "vaultTimeoutActionLockDesc": { - "message": "Απαιτείται κύριος κωδικός πρόσβασης ή άλλη μέθοδος ξεκλειδώματος για να αποκτήσετε ξανά πρόσβαση στο θησαυ/κιό σας." + "message": "Απαιτείται κύριος κωδικός πρόσβασης ή άλλη μέθοδος ξεκλειδώματος για να αποκτήσετε ξανά πρόσβαση στη κρύπτη σας." }, "vaultTimeoutActionLogOutDesc": { - "message": "Απαιτείται αυθεντικοποίηση για να αποκτήσετε ξανά πρόσβαση στο θησαυ/κιό σας." + "message": "Απαιτείται αυθεντικοποίηση για να αποκτήσετε ξανά πρόσβαση στη κρύπτη σας." }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου λήξης θησαυ/κίου." @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Τα βιομετρικά στον περιηγητή απαιτούν την ενεργοποίηση των βιομετρικών επιφάνειας εργασίας στις ρυθμίσεις πρώτα." }, + "biometricsManualSetupTitle": { + "message": "Η αυτόματη ρύθμιση δεν είναι διαθέσιμη" + }, + "biometricsManualSetupDesc": { + "message": "Λόγω της μεθόδου εγκατάστασης, η υποστήριξη των βιομετρικών δεν μπορεί να ενεργοποιηθεί αυτόματα. Θα θέλατε να ανοίξετε το εγχειρίδιο για το πώς να το κάνετε αυτό χειροκίνητα;" + }, "personalOwnershipSubmitError": { "message": "Λόγω μιας επιχειρηματικής πολιτικής, περιορίζεστε από την αποθήκευση αντικειμένων στο ατομικό σας θησαυ/κιό. Αλλάξτε την επιλογή ιδιοκτησίας σε έναν οργανισμό και επιλέξτε από τις διαθέσιμες συλλογές." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ο κύριος κωδικός πρόσβασής σας δεν πληροί μία ή περισσότερες πολιτικές του οργανισμού σας. Για να αποκτήσετε πρόσβαση στο θησαυ/κιο, πρέπει να ενημερώσετε τον κύριο κωδικό πρόσβασής σας τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ο οργανισμός σας έχει απενεργοποιήσει την κρυπτογράφηση αξιόπιστης συσκευής. Παρακαλώ ορίστε έναν κύριο κωδικό πρόσβασης για να αποκτήσετε πρόσβαση στο θησαυροφυλάκιο σας." + }, "tryAgain": { "message": "Προσπαθήστε ξανά" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Δεδομένα" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 62ae99fc7b8..e24cd7b6412 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on launch" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on launch" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Automatic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organisation and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organisation has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index ced2fc61e31..d09708b972d 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on launch" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on launch" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be enabled in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Automatic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organisation has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 9cf5ef6f60b..eed26bd0106 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index d71866a8f18..bf331ce430a 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Ajustes adicionales de Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verificar para Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Desbloquear con Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Solicitar Windows Hello al iniciar" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Solicitar Touch ID al iniciar" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "La biometría del navegador requiere habilitar primero la biometría de escritorio en los ajustes." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Debido a una política de organización, tiene restringido el guardar elementos a su caja fuerte personal. Cambie la configuración de propietario a organización y elija entre las colecciones disponibles." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Tu contraseña maestra no cumple con una o más de las políticas de tu organización. Para acceder a la caja fuerte, debes actualizar tu contraseña maestra ahora. Proceder te desconectará de tu sesión actual, requiriendo que vuelva a iniciar sesión. Las sesiones activas en otros dispositivos pueden seguir estando activas durante hasta una hora." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Intentar de nuevo" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index f746cbb30f1..5eb1a6fe0e1 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Kinnita Bitwardenisse sisselogimine." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Lukusta lahti Touch ID-ga" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Küsi avamisel Windows Hello tuvastust" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Küsi avamisel Touch ID tuvastust" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Selleks, et kasutada biomeetriat brauseris, peab selle esmalt Bitwardeni töölaua rakenduse seadetes sisse lülitama." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Ettevõtte poliitika tõttu ei saa sa andmeid oma personaalsesse Hoidlasse salvestada. Vali Omanikuks organisatsioon ja vali mõni saadavaolevatest Kogumikest." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Sinu ülemparool ei vasta ühele või rohkemale organisatsiooni poolt seatud poliitikale. Hoidlale ligipääsemiseks pead oma ülemaprooli uuendama. Jätkamisel logitakse sind praegusest sessioonist välja, mistõttu pead uuesti sisse logima. Teistes seadmetes olevad aktiivsed sessioonid aeguvad umbes ühe tunni jooksul." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index c0f8e046429..146e6e31b83 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Egiaztatu Bitwarden-entzako." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Desblokeatu Touch ID-arekin" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Eskatu Windows Hello abiaraztean" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Eskatu Touch ID abiaraztean" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Nabigatzailearen biometriak lehenik mahaigainaren biometria gaitzeko eskatzen du." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Erakundeko politika bat dela eta, ezin dituzu elementuak zure kutxa gotor pertsonalean gorde. Aldatu jabe aukera erakunde aukera batera, eta aukeratu bilduma erabilgarrien artean." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index d435e1fdde3..613fec6bedc 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "تنظیمات اضافی Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "تأیید برای Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "باز کردن با Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "درخواست Windows Hello در هنگام راه اندازی" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "درخواست Touch ID در هنگام راه اندازی" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "بیومتریک مرورگر ابتدا نیاز به فعالسازی بیومتریک دسکتاپ در تنظیمات دارد." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "به دلیل سیاست پرمیوم، برای ذخیره موارد در گاوصندوق شخصی خود محدود شده اید. گزینه مالکیت را به یک سازمان تغییر دهید و مجموعه های موجود را انتخاب کنید." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "کلمه عبور اصلی شما با یک یا چند سیاست سازمان‌تان مطابقت ندارد. برای دسترسی به گاوصندوق، باید همین حالا کلمه عبور اصلی خود را به‌روز کنید. در صورت ادامه، شما از نشست فعلی خود خارج می‌شوید و باید دوباره وارد سیستم شوید. نشست فعال در دستگاه های دیگر ممکن است تا یک ساعت همچنان فعال باقی بمانند." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "دوباره سعی کنید" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 6c1fa599281..8ad13891213 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Windows Hello -lisäasetukset." }, + "unlockWithPolkit": { + "message": "Avaa järjestelmän tunnistautumisella" + }, "windowsHelloConsentMessage": { "message": "Vahvista Bitwarden." }, + "polkitConsentMessage": { + "message": "Todenna avataksesi Bitwardenin lukitus." + }, "unlockWithTouchId": { "message": "Avaa Touch ID:llä" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Pyydä Windows Hello -todennusta käynnistettäessä" }, + "autoPromptPolkit": { + "message": "Pyydä järjestelmän tunnistautumista käynnistettäessä" + }, "autoPromptTouchId": { "message": "Pyydä Touch ID -todennusta käynnistettäessä" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometria selaimissa edellyttää sen määritystä työpöytäsovelluksen asetuksista." }, + "biometricsManualSetupTitle": { + "message": "Automaattinen määritys ei ole käytettävissä" + }, + "biometricsManualSetupDesc": { + "message": "Biometriatukea ei voitu ottaa automaattisesti käyttöön asennustavan vuoksi. Haluatko avata ohjeet tämän tekemiseksi manuaalisesti?" + }, "personalOwnershipSubmitError": { "message": "Yrityskäytännön johdosta kohteiden tallennus henkilökohtaiseen holviin ei ole mahdollista. Muuta omistusasetus organisaatiolle ja valitse käytettävissä olevista kokoelmista." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Pääsalasanasi ei täytä yhden tai useamman organisaatiokäytännön vaatimuksia ja holvin käyttämiseksi sinun on vaihdettava se nyt. Tämä uloskirjaa kaikki nykyiset istunnot pakottaen uudelleenkirjautumisen. Muiden laitteiden aktiiviset istunnot saattavat toimia vielä tunnin ajan." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Organisaatiosi on estänyt luotettavan laitesalauksen. Käytä holviasi asettamalla pääsalasana." + }, "tryAgain": { "message": "Yritä uudelleen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Tiedot" + }, + "fileSends": { + "message": "Tiedosto-Sendit" + }, + "textSends": { + "message": "Teksti-Sendit" } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 7e7d5485eea..326a86060a3 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify para sa Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "I-unlock gamit ang Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Humingi ng Windows Hello sa paglulunsad" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Humingi ng Touch ID sa paglulunsad" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Ang biometrics ng browser ay nangangailangan ng desktop biometrics na mai set up muna sa mga setting." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Dahil sa isang patakaran sa enterprise, pinaghihigpitan ka mula sa pag-save ng mga item sa iyong vault. Baguhin ang pagpipilian sa pagmamay ari sa isang organisasyon at pumili mula sa mga magagamit na koleksyon." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index f1e178410d8..481deb119f5 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Paramètres supplémentaires de Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Vérifier pour Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Déverrouiller avec Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Demander à Windows Hello au démarrage" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Demander Touch ID au démarrage de l'application" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Les options de biométrie dans le navigateur nécessitent au préalable l'activation des options de biométrie dans l'application de bureau." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "En raison d'une politique d'entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Sélectionnez une organisation dans l'option Propriété et choisissez parmi les collections disponibles." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Votre mot de passe principal ne répond pas aux exigences de politique de sécurité de cette organisation. Pour pouvoir accéder au coffre, vous devez mettre à jour votre mot de passe principal dès maintenant. En poursuivant, vous serez déconnecté de votre session actuelle et vous devrez vous reconnecter. Les sessions actives sur d'autres appareils peuver rester actives pendant encore une heure." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Essayez de nouveau" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 6d0573130ff..bc71a9703b1 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 61f784ce7eb..60fc0ef2264 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "הגדרות נוספות של Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "אימות עבור Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "שחרור נעילה עם Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "הצג את Windows Hello בפתיחת האפליקציה" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "הצג בקשה של Touch ID בפתיחת האפליקציה" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "בכדי להשתמש באמצעי אימות ביומטריים בדפדפן, אפשר תכונה זו באפליקציה בשולחן העבודה." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "בשל מדיניות ארגונית, אתה מוגבל לשמירת פריטים בכספת האישית שלך. שנה את ההגדרות בעלות החשבון לחשבון ארגוני ובחר מתוך האוספים הזמינים." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "לנסות שוב" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index a0eedecd256..d1edf8716bc 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index b5eaec06bcb..4180773f2b9 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -239,7 +239,7 @@ "message": "gđica." }, "mx": { - "message": "Mx" + "message": "gx." }, "dr": { "message": "dr." @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Dodatne Windows Hello postavke" }, + "unlockWithPolkit": { + "message": "Otključaj autentifikacijom sustava" + }, "windowsHelloConsentMessage": { "message": "Otključaj trezor." }, + "polkitConsentMessage": { + "message": "Otključaj Bitwarden autentifikacijom." + }, "unlockWithTouchId": { "message": "Otključaj koristeći Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Zahtijevaj Windows Hello pri pokretanju" }, + "autoPromptPolkit": { + "message": "Traži autentifikaciju sustava pri pokretanju" + }, "autoPromptTouchId": { "message": "Zahtijevaj Touch ID pri pokretanju" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrija preglednika zahtijeva prethodno omogućenu biometriju u Bitwarden desktop aplikaciji." }, + "biometricsManualSetupTitle": { + "message": "Automatsko postavljanje nije dostupno" + }, + "biometricsManualSetupDesc": { + "message": "Zbog načina instalacije, biometrijska podrška nije mogla biti automatski omogućena. Želiš li otvoriti dokumentaciju o tome kako to učiniti ručno?" + }, "personalOwnershipSubmitError": { "message": "Pravila tvrtke onemogućuju spremanje stavki u osobni trezor. Promijeni vlasništvo stavke na tvrtku i odaberi dostupnu Zbirku." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Tvoja glavna lozinka ne zadovoljava pravila ove organizacije. Za pristup trezoru moraš odmah ažurirati svoju glavnu lozinku. Ako nastaviš, odjaviti ćeš se iz trenutne sesije te ćeš se morati ponovno prijaviti. Aktivne sesije na drugim uređajima mogu ostati aktivne do jedan sat." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Tvoja je organizacija onemogućila šifriranje pouzdanog uređaja. Postavi glavnu lozinku za pristup svom trezoru." + }, "tryAgain": { "message": "Pokušaj ponovno" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Podaci" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index e699667a6a4..12c0ae560a8 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Kiegészítő Windows Hello beállítások" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bitwarden ellenőrzés." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Feloldás Touch ID segítségével" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Windows Hello kérése indításkor" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Érintés AZ kérése indításkor" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "A böngésző biometrikus adataihoz először az asztali biometrikus adatokat kell engedélyezni a beállításokban." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Egy vállalati házirend miatt korlátozásra került az elemek személyes tárolóba történő mentése. Módosítsuk a Tulajdon opciót egy szervezetre és válasszunk az elérhető gyűjtemények közül." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "A mesterjelszó nem felel meg egy vagy több szervezeti szabályzatnak. A széf eléréséhez frissíteni kell a meszerjelszót. A továbblépés kijelentkeztet az aktuális munkamenetből és újra be kell jelentkezni. A többi eszközön lévő aktív munkamenetek akár egy óráig is aktívak maradhatnak." }, + "tdeDisabledMasterPasswordRequired": { + "message": "A szervezete letiltotta a megbízható eszközök titkosítását. Állítsunk be egy mesterjelszót a széf eléréséhez." + }, "tryAgain": { "message": "Próbáluk újra" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Adat" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 2e822e292ac..b57fc49adf2 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verifikasi untuk Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Buka kunci dengan Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Minta Windows Hello saat diluncurkan" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Minta Touch ID saat diluncurkan" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrik browser mengharuskan biometrik desktop diaktifkan di pengaturan terlebih dahulu." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Karena Kebijakan Perusahaan, Anda dilarang menyimpan item ke lemari besi pribadi Anda. Ubah opsi Kepemilikan ke organisasi dan pilih dari Koleksi yang tersedia." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index ab23afe2773..4a0261f4cfb 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Impostazioni aggiuntive di Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verifica per Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Sblocca con Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Richiedi Windows Hello all'avvio" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Richiedi Touch ID all'avvio" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "L'autenticazione biometrica del browser richiede che l'autenticazione biometrica del desktop sia stata già impostata nelle impostazioni." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "A causa di una politica aziendale, non puoi salvare elementi nella tua cassaforte personale. Cambia l'opzione di proprietà in un'organizzazione e scegli tra le raccolte disponibili." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "La tua password principale non soddisfa uno o più politiche della tua organizzazione. Per accedere alla cassaforte, aggiornala ora. Procedere ti farà uscire dalla sessione corrente, richiedendoti di accedere di nuovo. Le sessioni attive su altri dispositivi potrebbero continuare a rimanere attive per un massimo di un'ora." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Riprova" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 8eb3510bc17..3067df84bb0 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "追加の Windows Hello 設定" }, + "unlockWithPolkit": { + "message": "システム認証でロック解除" + }, "windowsHelloConsentMessage": { "message": "Bitwarden の認証を行います。" }, + "polkitConsentMessage": { + "message": "認証して Bitwarden のロックを解除します。" + }, "unlockWithTouchId": { "message": "Touch ID でロック解除" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "起動時に Windows Hello を要求する" }, + "autoPromptPolkit": { + "message": "起動時にシステム認証を要求する" + }, "autoPromptTouchId": { "message": "起動時に Touch ID を要求する" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "ブラウザで生体認証を利用するには、最初に設定でデスクトップ生体認証を有効にする必要があります。" }, + "biometricsManualSetupTitle": { + "message": "自動設定は利用できません" + }, + "biometricsManualSetupDesc": { + "message": "インストール方法により、生体認証を自動的に有効化できませんでした。手動で設定する方法の説明を開きますか?" + }, "personalOwnershipSubmitError": { "message": "組織のポリシーにより、個人保管庫へのアイテムの保存が制限されています。 所有権を組織に変更し、利用可能なコレクションから選択してください。" }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "マスターパスワードが組織のポリシーに適合していません。保管庫にアクセスするには、今すぐマスターパスワードを更新しなければなりません。続行すると現在のセッションからログアウトし、再度ログインする必要があります。 他のデバイス上のアクティブなセッションは、最大1時間アクティブであり続けることがあります。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "あなたの組織は信頼できるデバイスの暗号化を無効化しました。保管庫にアクセスするにはマスターパスワードを設定してください。" + }, "tryAgain": { "message": "再試行" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "データ" + }, + "fileSends": { + "message": "ファイル Send" + }, + "textSends": { + "message": "テキスト Send" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 6d0573130ff..bc71a9703b1 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 6d0573130ff..bc71a9703b1 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 46fbbb6d230..cdf612baac0 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್‌ಗಾಗಿ ಪರಿಶೀಲಿಸಿ." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "ಟಚ್ ಐಡಿ ಯೊಂದಿಗೆ ಅನ್ಲಾಕ್ ಮಾಡಿ" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "ಬ್ರೌಸರ್ ಬಯೋಮೆಟ್ರಿಕ್ಸ್ ಮೊದಲು ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ಡೆಸ್ಕ್ಟಾಪ್ ಬಯೋಮೆಟ್ರಿಕ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕಾಗುತ್ತದೆ." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 01d8550a720..c7c22ccbd64 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bitwarden에서 인증을 요청합니다." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Touch ID를 사용하여 잠금 해제" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "실행 시 Windows Hello 요구하기" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "실행 시 Touch ID 요구하기" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "브라우저에서 생체 인식을 사용하기 위해서는 설정에서 데스크톱 생체 인식을 먼저 활성화해야 합니다." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "엔터프라이즈 정책으로 인해 개인 보관함에 항목을 저장할 수 없습니다. 조직에서 소유권 설정을 변경한 다음, 사용 가능한 컬렉션 중에서 선택해주세요." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index bca956d3e3c..5ce46436e35 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Papildomi Windows Hello nustatymai" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Patvirtinti Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Atrakinti naudojant Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Paprašyti Windows Hello paleidus programą" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Prašyti Touch ID paleidus programėlę" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Pirma reikia nustatymuose nustatyti darbalaukio biometrinius duomenys, prieš juos naudojant naršyklėje." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Dėl įmonės politikos jums neleidžiama saugoti daiktų asmeninėje saugykloje. Pakeiskite nuosavybės parinktį į organizaciją ir pasirinkite iš galimų rinkinių." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Jūsų pagrindinis slaptažodis neatitinka vieno ar kelių organizacijos slaptažodžiui keliamų reikalavimų. Norėdami prisijungti prie saugyklos, jūs turite atnaujinti savo pagrindinį slaptažodį. Jeigu nuspręsite tęsti, jūs būsite atjungti nuo dabartinės sesijos ir jums reikės vėl prisijungti. Visos aktyvios sesijos kituose įrenginiuose gali išlikti aktyvios iki vienos valandos." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Bandyti dar kartą" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index ef698a0b35c..6480f8dadae 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Windows Hello papildu iestatījumi" }, + "unlockWithPolkit": { + "message": "Atslēgt ar sistēmas autentifikāciju" + }, "windowsHelloConsentMessage": { "message": "Apstiprināt Bitwarden." }, + "polkitConsentMessage": { + "message": "Autentificēt, lai atslēgtu Bitwarden." + }, "unlockWithTouchId": { "message": "Atslēgt ar Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Palaišanā vaicāt pēc Windows Hello" }, + "autoPromptPolkit": { + "message": "Palaišanas laikā vaicāt pēc autentifikācijas" + }, "autoPromptTouchId": { "message": "Palaišanā vaicāt pēc Touch ID" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Vispirms ir nepieciešams iespējot biometriju darbvirsmas iestatījumos, lai to varētu izmantot pārlūkā." }, + "biometricsManualSetupTitle": { + "message": "Automātiskā uzstādīšana nav pieejama" + }, + "biometricsManualSetupDesc": { + "message": "Uzstādīšanas veida dēļ nevarēja automātiski iespējot biometrijas nodrošinājumu. Vai atvērt dokumentāciju par to, kā to izdarīt pašrocīgi?" + }, "personalOwnershipSubmitError": { "message": "Uzņēmuma nosacījumi liedz saglabāt vienumus privātajā glabātavā. Ir jānorāda piederība apvienībai un jāizvēlas kāds no pieejamajiem krājumiem." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Galvenā parole neatbilst vienam vai vairākiem apvienības nosacījumiem. Ir jāatjaunina galvenā parole, lai varētu piekļūt glabātavai. Turpinot notiks atteikšanās no pašreizējās sesijas, un būs nepieciešams pieteikties no jauna. Citās ierīcēs esošās sesijas var turpināt darboties līdz vienai stundai." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Tava apvienība ir atspējojusi uzticamo ierīču šifrēšanu. Lūgums iestatīt galveno paroli, lai piekļūtu savai glabātavai." + }, "tryAgain": { "message": "Jāmēģina vēlreiz" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Dati" + }, + "fileSends": { + "message": "Datņu Send" + }, + "textSends": { + "message": "Teksta Send" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 6c70b5e5e6c..dd6850cfa60 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verifikuj za Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Otključaj sa Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index bd1e9dbd15f..bba2772f621 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bitwarden വേണ്ടി പരിശോധിച്ചുറപ്പിക്കുക." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Touch ID ഉപയോഗിച്ച് അൺലോക്കുചെയ്യുക" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 6d0573130ff..bc71a9703b1 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index e09bc197d72..a771badfb0e 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index fb2430e75e1..50819e4968f 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -500,7 +500,7 @@ "message": "Opprett en konto" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Velg et sterkt passord" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -552,7 +552,7 @@ "message": "Master password hint" }, "joinOrganization": { - "message": "Join organization" + "message": "Bli med i organisasjon" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." @@ -595,7 +595,7 @@ "message": "You successfully logged in" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Du kan lukke dette vinduet" }, "masterPassDoesntMatch": { "message": "Superpassord-bekreftelsen er ikke samsvarende." @@ -634,7 +634,7 @@ "message": "En verifiseringskode er påkrevd." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "Autentiseringen ble avbrutt eller tok for lang tid. Prøv igjen." }, "invalidVerificationCode": { "message": "Ugyldig verifiseringskode" @@ -799,10 +799,10 @@ "message": "Din innloggingsøkt har utløpt." }, "restartRegistration": { - "message": "Restart registration" + "message": "Start registrering på nytt" }, "expiredLink": { - "message": "Expired link" + "message": "Utløpt lenke" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -1382,7 +1382,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "Filpassord" }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" @@ -1400,7 +1400,7 @@ "message": "Export type" }, "accountRestricted": { - "message": "Account restricted" + "message": "Konto begrenset" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "“File password” and “Confirm file password“ do not match." @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Ytterligere Windows Hello-innstillinger" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bekreft for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Lås opp med Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Spør etter Windows Hello ved oppstart" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Spør om Touch ID ved oppstart" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometri i nettleserutvidelsen krever først aktivering i innstillinger i skrivebordsprogrammet." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "På grunn av bedrifsretningslinjer er du begrenset fra å lagre objekter til ditt personlige hvelv. Endre alternativ for eierskap til en organisasjon og velg blant tilgjengelige samlinger." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Prøv igjen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index ecce20d5cb8..847da864d3e 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index f143382c846..ebceb9dbe8e 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -442,10 +442,10 @@ "description": "Minimum Special Characters" }, "ambiguous": { - "message": "Dubbelzinnige tekens vermijden" + "message": "Vermijd dubbelzinnige tekens" }, "searchCollection": { - "message": "Verzameling doorzoeken" + "message": "Doorzoek collectie" }, "searchFolder": { "message": "Map doorzoeken" @@ -458,22 +458,22 @@ "description": "Search item type" }, "newAttachment": { - "message": "Nieuwe bijlage toevoegen" + "message": "Voeg nieuwe bijlage toe" }, "deletedAttachment": { - "message": "Bijlage is verwijderd" + "message": "Bijlage verwijderd" }, "deleteAttachmentConfirmation": { "message": "Weet je zeker dat je deze bijlage wilt verwijderen?" }, "attachmentSaved": { - "message": "De bijlage is opgeslagen." + "message": "Bijlage opgeslagen" }, "file": { "message": "Bestand" }, "selectFile": { - "message": "Selecteer een bestand." + "message": "Selecteer een bestand" }, "maxFileSize": { "message": "Maximale bestandsgrootte is 500 MB." @@ -491,22 +491,22 @@ "message": "Weet je zeker dat je deze map wilt verwijderen?" }, "deletedFolder": { - "message": "Map is verwijderd" + "message": "Map verwijderd" }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." }, "createAccount": { - "message": "Account aanmaken" + "message": "Maak een account aan" }, "setAStrongPassword": { - "message": "Sterk wachtwoord instellen" + "message": "Stel een sterk wachtwoord in" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Rond het aanmaken van je account af met het instellen van een wachtwoord" }, "logIn": { - "message": "Inloggen" + "message": "Log in" }, "submit": { "message": "Opslaan" @@ -552,7 +552,7 @@ "message": "Hoofdwachtwoordhint" }, "joinOrganization": { - "message": "Lid van organisatie worden" + "message": "Join organisatie" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Voltooi je lidmaatschap aan deze organisatie door een hoofdwachtwoord in te stellen." @@ -610,16 +610,16 @@ "message": "Er is een onverwachte fout opgetreden." }, "itemInformation": { - "message": "Item" + "message": "Item-informatie" }, "noItemsInList": { "message": "Er zijn geen items om weer te geven." }, "sendVerificationCode": { - "message": "Stuur een verificatiecode naar je e-mail" + "message": "Stuur verificatiecode naar je e-mail" }, "sendCode": { - "message": "Code versturen" + "message": "Verstuur code" }, "codeSent": { "message": "Code verstuurd" @@ -698,7 +698,7 @@ "message": "Gebruik een YubiKey om toegang te krijgen tot je account. Werkt met YubiKey 4, 4 Nano, 4C en Neo-apparaten." }, "duoDescV2": { - "message": "Door Duo Security gegenereerde code invoeren.", + "message": "Voer een door Duo Security gegenereerde code in.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -847,7 +847,7 @@ "message": "Hulp en feedback" }, "getHelp": { - "message": "Hulp vragen" + "message": "Vraag ondersteuning" }, "fileBugReport": { "message": "Rapporteer een fout (bug)" @@ -882,10 +882,10 @@ "message": "Ga naar de webkluis" }, "getMobileApp": { - "message": "Download de mobiele app" + "message": "Download mobiele app" }, "getBrowserExtension": { - "message": "Download de browserextensie" + "message": "Download browserextensie" }, "syncingComplete": { "message": "Synchronisatie voltooid" @@ -979,7 +979,7 @@ "message": "Beveiliging" }, "clearClipboard": { - "message": "Klembord wissen", + "message": "Wis klembord", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -987,7 +987,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Websitepictogrammen weergeven" + "message": "Toon website-pictogrammen" }, "faviconDesc": { "message": "Een herkenbare afbeelding naast iedere login weergeven." @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Extra Windows Hello-instellingen" }, + "unlockWithPolkit": { + "message": "Ontgrendel met systeemauthenticatie" + }, "windowsHelloConsentMessage": { "message": "Verifiëren voor Bitwarden." }, + "polkitConsentMessage": { + "message": "Verifieer om Bitwarden te ontgrendelen." + }, "unlockWithTouchId": { "message": "Ontgrendelen met Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Vraag om Windows Hello bij opstarten" }, + "autoPromptPolkit": { + "message": "Vraag naar systeemverificatie bij het opstarten" + }, "autoPromptTouchId": { "message": "Vraag om Touch ID bij opstarten" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Voor browserbiometrie moet je eerst desktopbiometrie inschakelen in de instellingen." }, + "biometricsManualSetupTitle": { + "message": "Automatisch installeren niet beschikbaar" + }, + "biometricsManualSetupDesc": { + "message": "Vanwege de installatiemethode kon biometrische ondersteuning niet automatisch worden ingeschakeld. Wil je de documentatie lezen over hoe je dit handmatig kunt doen?" + }, "personalOwnershipSubmitError": { "message": "Wegens bedrijfsbeleid mag je geen wachtwoorden opslaan in je persoonlijke kluis. Verander het eigenaarschap naar een organisatie en kies uit een van de beschikbare collecties." }, @@ -1977,7 +1992,7 @@ "message": "Wordt verwijderd" }, "webAuthnAuthenticate": { - "message": "Authenticeer WebAuthn" + "message": "Verifieer WebAuthn" }, "hideEmail": { "message": "Verberg mijn e-mailadres voor ontvangers." @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Je hoofdwachtwoord voldoet niet aan en of meerdere oganisatiebeleidsonderdelen. Om toegang te krijgen tot de kluis, moet je je hoofdwachtwoord nu bijwerken. Doorgaan zal je huidige sessie uitloggen, waarna je opnieuw moet inloggen. Actieve sessies op andere apparaten blijven mogelijk nog een uur actief." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Je organisatie heeft het versleutelen van vertrouwde apparaten uitgeschakeld. Stel een hoofdwachtwoord in om toegang te krijgen tot je kluis." + }, "tryAgain": { "message": "Opnieuw proberen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Gegevens" + }, + "fileSends": { + "message": "Bestand-Sends" + }, + "textSends": { + "message": "Tekst-Sends" } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ca99bfcbe47..c19b45212ea 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index da8b7401ffc..b82d3631bae 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 06ceca87e97..20258cc6a49 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Dodatkowe ustawienia Windows Hello" }, + "unlockWithPolkit": { + "message": "Odblokuj za pomocą uwierzytelniania systemowego" + }, "windowsHelloConsentMessage": { "message": "Zweryfikuj dla Bitwarden." }, + "polkitConsentMessage": { + "message": "Uwierzytelnij, aby odblokować Bitwarden." + }, "unlockWithTouchId": { "message": "Odblokuj za pomocą Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Poproś o Windows Hello przy uruchomieniu" }, + "autoPromptPolkit": { + "message": "Zapytaj o uwierzytelnianie systemowe przy uruchomieniu" + }, "autoPromptTouchId": { "message": "Poproś o Touch ID przy uruchomieniu" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Aby włączyć dane biometryczne w przeglądarce, musisz włączyć tę samą funkcję w ustawianiach aplikacji." }, + "biometricsManualSetupTitle": { + "message": "Automatyczna konfiguracja niedostępna" + }, + "biometricsManualSetupDesc": { + "message": "Ze względu na metodę instalacji, biometria nie może być automatycznie włączona. Czy chcesz otworzyć dokumentację dotyczącą tego, jak to zrobić ręcznie?" + }, "personalOwnershipSubmitError": { "message": "Ze względu na zasadę przedsiębiorstwa, nie możesz zapisywać elementów w osobistym sejfie. Zmień właściciela elementu na organizację i wybierz jedną z dostępnych kolekcji." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Twoje hasło główne nie spełnia jednej lub kilku zasad organizacji. Aby uzyskać dostęp do sejfu, musisz teraz zaktualizować swoje hasło główne. Kontynuacja wyloguje Cię z bieżącej sesji, wymagając zalogowania się ponownie. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie jedną godzinę." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Spróbuj ponownie" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Dane" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index b330517b37e..b3d00976b6f 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -45,7 +45,7 @@ "message": "Compartilhar" }, "moveToOrganization": { - "message": "Mover para a Organização" + "message": "Mover para a organização" }, "movedItemToOrg": { "message": "$ITEMNAME$ movido para $ORGNAME$", @@ -527,7 +527,7 @@ "message": "Dica da Senha Mestra (opcional)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se você esquecer sua senha, a dica da senha pode ser enviada ao seu e-mail. $CURRENT$/$MAXIMUM$ caracteres máximos.", "placeholders": { "current": { "content": "$1", @@ -540,22 +540,22 @@ } }, "masterPassword": { - "message": "Master password" + "message": "Senha mestra" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Confirme a senha mestra" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Dica da senha mestra" }, "joinOrganization": { - "message": "Join organization" + "message": "Juntar-se à organização" }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Termine de juntar-se nessa organização definindo uma senha mestra." }, "settings": { "message": "Configurações" @@ -634,7 +634,7 @@ "message": "Requer o código de verificação." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "A autenticação foi cancelada ou demorou muito. Por favor tente novamente." }, "invalidVerificationCode": { "message": "Código de verificação inválido" @@ -688,17 +688,17 @@ "message": "Aplicativo de Autenticação" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Insira um código gerado por um aplicativo autenticador como o Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Chave de segurança Yubico OTP" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para acessar a sua conta. Funciona com YubiKey 4, 4 Nano, 4C, e dispositivos NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Insira um código gerado pelo Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -715,7 +715,7 @@ "message": "E-mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Digite o código enviado para seu e-mail." }, "loginUnavailable": { "message": "Sessão Indisponível" @@ -799,16 +799,16 @@ "message": "A sua sessão expirou." }, "restartRegistration": { - "message": "Restart registration" + "message": "Reiniciar registro" }, "expiredLink": { - "message": "Expired link" + "message": "Link expirado" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Por favor, reinicie o registro ou tente fazer login." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Você pode já ter uma conta" }, "logOutConfirmation": { "message": "Você tem certeza que deseja sair?" @@ -979,7 +979,7 @@ "message": "Segurança" }, "clearClipboard": { - "message": "Limpar Área de Transferência", + "message": "Limpar área de transferência", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -993,7 +993,7 @@ "message": "Mostre uma imagem reconhecível ao lado de cada credencial." }, "enableMinToTray": { - "message": "Minimizar para Ícone da Bandeja" + "message": "Minimizar para ícone da bandeja" }, "enableMinToTrayDesc": { "message": "Ao minimizar a janela, mostra um ícone na bandeja do sistema." @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Configurações adicionais do Windows Hello" }, + "unlockWithPolkit": { + "message": "Desbloquear com autenticação de sistema" + }, "windowsHelloConsentMessage": { "message": "Verifique para o Bitwarden." }, + "polkitConsentMessage": { + "message": "Autentice para desbloquear o Bitwarden." + }, "unlockWithTouchId": { "message": "Desbloquear com o Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Perguntar para iniciar o Hello do Windows" }, + "autoPromptPolkit": { + "message": "Pedir autenticação do sistema na inicialização" + }, "autoPromptTouchId": { "message": "Pedir pelo Touch ID ao iniciar" }, @@ -1712,7 +1721,7 @@ "message": "A sua nova senha mestra não cumpre aos requisitos da política." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Obtenha conselhos, novidades, e oportunidades de pesquisa do Bitwarden em sua caixa de entrada." }, "unsubscribe": { "message": "Cancelar subscrição" @@ -1802,7 +1811,13 @@ "message": "Biometria não ativada" }, "biometricsNotEnabledDesc": { - "message": "A biometria com o navegador requer que a biometria de desktop seja ativada nas configurações primeiro." + "message": "A biometria do navegador exige que a biometria do desktop seja configurada primeiro nas configurações." + }, + "biometricsManualSetupTitle": { + "message": "Configuração automática não disponível" + }, + "biometricsManualSetupDesc": { + "message": "Devido ao método de instalação, o suporte a dados biométricos não pôde ser ativado automaticamente. Você gostaria de abrir a documentação sobre como fazer isso manualmente?" }, "personalOwnershipSubmitError": { "message": "Devido a uma Política Empresarial, você está restrito de salvar itens para seu cofre pessoal. Altere a opção de Propriedade para uma organização e escolha entre as Coleções disponíveis." @@ -1866,7 +1881,7 @@ "message": "Contagem Atual de Acessos" }, "disableSend": { - "message": "Desative este Send para que ninguém possa acessá-lo.", + "message": "Desative este envio para que ninguém possa acessá-lo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { @@ -1882,7 +1897,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinkLabel": { - "message": "Link do Send", + "message": "Enviar link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "textHiddenByDefault": { @@ -1890,26 +1905,26 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Send Criado", + "message": "Envio adicionado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Send Editado", + "message": "Envio salvo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { - "message": "Send Excluído", + "message": "Enviar excluído", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "Nova Senha" + "message": "Nova senha" }, "whatTypeOfSend": { "message": "Que tipo de Send é este?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { - "message": "Criar Send", + "message": "Novo envio", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { @@ -1945,7 +1960,7 @@ "message": "Copiar o link para compartilhar este Send para minha área de transferência ao salvar." }, "sendDisabled": { - "message": "Send desativado", + "message": "Envio removido", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -1986,10 +2001,10 @@ "message": "Uma ou mais políticas da organização estão afetando as suas opções de Send." }, "emailVerificationRequired": { - "message": "Verificação de E-mail Necessária" + "message": "Verificação de e-mail necessária" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "E-mail verificado" }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso." @@ -2004,7 +2019,7 @@ "message": "Esta ação está protegida. Para continuar, por favor, reinsira a sua senha mestra para verificar sua identidade." }, "updatedMasterPassword": { - "message": "Senha Mestra Atualizada" + "message": "Senha mestra atualizada" }, "updateMasterPassword": { "message": "Atualizar Senha Mestra" @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Sua organização desativou a criptografia confiável do dispositivo. Por favor, defina uma senha mestra para acessar o seu cofre." + }, "tryAgain": { "message": "Tentar novamente" }, @@ -2058,7 +2076,7 @@ "message": "Minutos" }, "vaultTimeoutPolicyInEffect": { - "message": "As políticas da sua organização estão afetando o tempo limite do seu cofre. O Tempo Limite Máximo permitido do Cofre é $HOURS$ hora(s) e $MINUTES$ minuto(s)", + "message": "As políticas da sua organização definiram o tempo limite máximo permitido do cofre para $HOURS$ hora(s) e $MINUTES$ minuto(s).", "placeholders": { "hours": { "content": "$1", @@ -2100,10 +2118,10 @@ "message": "O tempo limite do seu cofre excede as restrições definidas por sua organização." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Convite aceito" }, "resetPasswordPolicyAutoEnroll": { - "message": "Inscrição Automática" + "message": "Inscrição automática" }, "resetPasswordAutoEnrollInviteWarning": { "message": "Esta organização possui uma política empresarial que irá inscrevê-lo automaticamente na redefinição de senha. A inscrição permitirá que os administradores da organização alterem sua senha mestra." @@ -2217,23 +2235,23 @@ "message": "O que você gostaria de gerar?" }, "passwordType": { - "message": "Tipo de Senha" + "message": "Tipo de senha" }, "regenerateUsername": { - "message": "Regenerar Usuário" + "message": "Gerar nome de usuário novamente" }, "generateUsername": { - "message": "Gerar Usuário" + "message": "Gerar usuário" }, "usernameType": { - "message": "Tipo de Usuário" + "message": "Tipo de usuário" }, "plusAddressedEmail": { - "message": "E-mail alternativo (com um +)", + "message": "Mais e-mail endereçado", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use as capacidades de sub-endereçamento do seu provedor de e-mail." + "message": "Use os recursos de subendereçamento do seu provedor de e-mail." }, "catchallEmail": { "message": "E-mail pega-tudo" @@ -2245,7 +2263,7 @@ "message": "Aleatório" }, "randomWord": { - "message": "Palavra Aleatória" + "message": "Palavra aleatória" }, "websiteName": { "message": "Nome do site" @@ -2745,7 +2763,7 @@ "message": "Submenu" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ativar/desativar navegação lateral" }, "skipToContent": { "message": "Ir para o conteúdo" @@ -2803,7 +2821,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Erro ao se conectar com o serviço Duo. Use um método de verificação de duas etapas diferente ou contate o Duo para assistência." }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicie o Duo e siga os passos para finalizar o login." @@ -2827,7 +2845,7 @@ "message": "Senha do arquivo inválida, por favor informe a senha utilizada quando criou o arquivo de exportação." }, "destination": { - "message": "Destination" + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre suas opções de importação" @@ -2886,7 +2904,7 @@ "message": "Confirmar senha do arquivo" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dados do cofre exportados" }, "multifactorAuthenticationCancelled": { "message": "Autenticação de múltiplos fatores cancelada" @@ -3024,6 +3042,12 @@ } }, "data": { - "message": "Data" + "message": "Dado" + }, + "fileSends": { + "message": "Arquivos enviados" + }, + "textSends": { + "message": "Texto enviado" } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 7e98dcae50e..e95095e56c7 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Definições adicionais do Windows Hello" }, + "unlockWithPolkit": { + "message": "Desbloqueio com autenticação do sistema" + }, "windowsHelloConsentMessage": { "message": "Verificar para o Bitwarden." }, + "polkitConsentMessage": { + "message": "Autenticar para desbloquear o Bitwarden." + }, "unlockWithTouchId": { "message": "Desbloquear com Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Pedir o Windows Hello ao iniciar a aplicação" }, + "autoPromptPolkit": { + "message": "Pedir a autenticação do sistema no arranque" + }, "autoPromptTouchId": { "message": "Pedir o Touch ID ao iniciar a aplicação" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "A biometria do navegador requer que a biometria do computador seja primeiro configurada nas definições." }, + "biometricsManualSetupTitle": { + "message": "Configuração automática não disponível" + }, + "biometricsManualSetupDesc": { + "message": "Devido ao método de instalação, não foi possível ativar automaticamente o suporte biométrico. Gostaria de abrir a documentação sobre como o fazer manualmente?" + }, "personalOwnershipSubmitError": { "message": "Devido a uma política empresarial, está impedido de guardar itens no seu cofre pessoal. Altere a opção Propriedade para uma organização e escolha entre as coleções disponíveis." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "A sua palavra-passe mestra não cumpre uma ou mais políticas da sua organização. Para aceder ao cofre, tem de atualizar a sua palavra-passe mestra agora. Ao prosseguir, terminará a sua sessão atual e terá de iniciar sessão novamente. As sessões ativas noutros dispositivos poderão continuar ativas até uma hora." }, + "tdeDisabledMasterPasswordRequired": { + "message": "A sua organização desativou a encriptação de dispositivos fiáveis. Por favor, defina uma palavra-passe mestra para aceder ao seu cofre." + }, "tryAgain": { "message": "Tentar novamente" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Dados" + }, + "fileSends": { + "message": "Sends de ficheiros" + }, + "textSends": { + "message": "Sends de texto" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index e62db26b816..209fc4dc3e5 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verificați pentru Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Deblocare cu Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Solicitați Windows Hello la pornire" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Solicitați Touch ID la pornire" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometria browserului necesită ca mai întâi să fie configurată biometria desktopului în setări." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Din cauza unei politici a întreprinderii, nu vă puteți salva elemente în seiful individual. Schimbați opțiunea de proprietate la o organizație și alegeți din colecțiile disponibile." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index fdb2e124830..c4770627505 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Дополнительные настройки Windows Hello" }, + "unlockWithPolkit": { + "message": "Разблокировать с помощью системной аутентификации" + }, "windowsHelloConsentMessage": { "message": "Верификация для Bitwarden." }, + "polkitConsentMessage": { + "message": "Для разблокировки Bitwarden пройдите аутентификацию." + }, "unlockWithTouchId": { "message": "Разблокировать с Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Запрашивать Windows Hello при запуске приложения" }, + "autoPromptPolkit": { + "message": "Запрашивать системную аутентификацию при запуске" + }, "autoPromptTouchId": { "message": "Запрашивать Touch ID при запуске приложения" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Для активации биометрии в браузере сначала необходимо включить биометрию в приложении для компьютера." }, + "biometricsManualSetupTitle": { + "message": "Автоматическая настройка недоступна" + }, + "biometricsManualSetupDesc": { + "message": "Из-за метода инсталляции не удалось автоматически включить поддержку биометрии. Вы хотите открыть документацию чтобы узнать, как это сделать вручную?" + }, "personalOwnershipSubmitError": { "message": "В соответствии с корпоративной политикой вам запрещено сохранять элементы в личном хранилище. Измените владельца на организацию и выберите из доступных Коллекций." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш мастер-пароль не соответствует требованиям политики вашей организации. Для доступа к хранилищу вы должны обновить свой мастер-пароль прямо сейчас. При этом текущий сеанс будет завершен и потребуется повторная авторизация. Сеансы на других устройствах могут оставаться активными в течение часа." }, + "tdeDisabledMasterPasswordRequired": { + "message": "В вашей организации отключено шифрование доверенных устройств. Пожалуйста, установите мастер-пароль для доступа к вашему хранилищу." + }, "tryAgain": { "message": "Попробуйте снова" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Данные" + }, + "fileSends": { + "message": "Файловая Send" + }, + "textSends": { + "message": "Текстовая Send" } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index b2f2469ccf3..29a230aba1e 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 5edd4f4880a..4d39730d0a7 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Ďalšie nastavenia Windows Hello" }, + "unlockWithPolkit": { + "message": "Odomknúť systémovým overením" + }, "windowsHelloConsentMessage": { "message": "Overiť sa pre Bitwarden." }, + "polkitConsentMessage": { + "message": "Overením odomknete Bitwarden." + }, "unlockWithTouchId": { "message": "Odomknúť s Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Pri spustení požiadať o Windows Hello" }, + "autoPromptPolkit": { + "message": "Pri spustení požiadať o systémové overenie" + }, "autoPromptTouchId": { "message": "Pri spustení požiadať o Touch ID" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometria prehliadača vyžaduje, aby bola najskôr v nastaveniach povolená biometria počítača." }, + "biometricsManualSetupTitle": { + "message": "Automatické nastavenie nie je k dispozícii" + }, + "biometricsManualSetupDesc": { + "message": "Vzhľadom na spôsob inštalácie nebolo možné automaticky povoliť podporu biometrie. Chcete otvoriť dokumentáciu, ako to urobiť manuálne?" + }, "personalOwnershipSubmitError": { "message": "Z dôvodu podnikovej politiky máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Vaše hlavné heslo nespĺňa jednu alebo viacero podmienok vašej organizácie. Ak chcete získať prístup k trezoru, musíte teraz aktualizovať svoje hlavné heslo. Pokračovaním sa odhlásite z aktuálnej relácie a budete sa musieť znova prihlásiť. Aktívne relácie na iných zariadeniach môžu zostať aktívne až jednu hodinu." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Vaša organizácia zakázala šifrovanie dôveryhodného zariadenia. Na prístup k trezoru nastavte hlavné heslo." + }, "tryAgain": { "message": "Skúsiť znova" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Údaje" + }, + "fileSends": { + "message": "Sendy so súborom" + }, + "textSends": { + "message": "Textové Sendy" } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 7916795d1a4..c19ec0ab516 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Preverite za Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Odkleni z biometriko" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Biometrično preverjanje ob zagonu aplikacije" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 172875b1ee5..13ec3acf231 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Додатна подешавања Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Потврди за Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Откључај са Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Захтевај Windows Hello при покретању" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Захтевај Touch ID при покретању" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Биометрија прегледача захтева да у поставкама прво буде омогућена биометрија desktop-а." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Због смерница за предузећа, ограничено вам је чување предмета у вашем личном трезору. Промените опцију власништва у организацију и изаберите из доступних колекција." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваша главна лозинка не испуњава једну или више смерница ваше организације. Да бисте приступили сефу, морате одмах да ажурирате главну лозинку. Ако наставите, одјавићете се са ваше тренутне сесије, што захтева да се поново пријавите. Активне сесије на другим уређајима могу да остану активне до један сат." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша организација је онемогућила шифровање поузданог уређаја. Поставите главну лозинку за приступ вашем трезору." + }, "tryAgain": { "message": "Покушајте поново" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Подаци" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index b164bb5f19e..77badeaa0bf 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Ytterligare inställningar för Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bekräfta för Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Lås upp med Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Be om Windows Hello vid appstart" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Be om Touch ID vid appstart" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Biometri i webbläsaren kräver att biometri på skrivbordet aktiveras i inställningarna först." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "På grund av en av företagets policyer är du begränsad från att spara objekt till ditt personliga valv. Ändra ägarskap till en organisation och välj från tillgängliga samlingar." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ditt huvudlösenord följer inte ett eller flera av din organisations regler. För att komma åt ditt valv så måste du ändra ditt huvudlösenord nu. Om du gör det kommer du att loggas du ut ur din nuvarande session så du måste logga in på nytt. Aktiva sessioner på andra enheter kommer fortsatt vara aktiva i upp till en timme." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Försök igen" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 6d0573130ff..bc71a9703b1 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 2cd71c42ebb..54bc05182b5 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Ask for Windows Hello on app start" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Ask for Touch ID on app start" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Try again" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 49579f05845..4fcd925335f 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Ekstra Windows Hello ayarları" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Bitwarden için doğrulayın." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Kilidi Touch ID ile aç" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Uygulamayı başlatırken Windows Hello doğrulaması iste" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Uygulamayı başlatırken Touch ID iste" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Tarayıcıda biyometriyi kullanmak için önce ayarlardan masaüstü biyometrisini ayarlamanız gerekir." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Bir kuruluş ilkesi nedeniyle kişisel kasanıza hesap kaydetmeniz kısıtlanmış. Sahip seçeneğini bir kuruluş olarak değiştirin ve mevcut koleksiyonlar arasından seçim yapın." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ana parolanız kuruluş ilkelerinizi karşılamıyor. Kasanıza erişmek için ana parolanızı güncellemelisiniz. Devam ettiğinizde oturumunuz kapanacak ve yeniden oturum açmanız gerekecektir. Diğer cihazlardaki aktif oturumlar bir saate kadar aktif kalabilir." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Yeniden dene" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Veri" + }, + "fileSends": { + "message": "Dosya Send'leri" + }, + "textSends": { + "message": "Metin Send'leri" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index b015beacb10..ce6697faf65 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -21,7 +21,7 @@ "message": "Картка" }, "typeIdentity": { - "message": "Особисті дані" + "message": "Посвідчення" }, "typeSecureNote": { "message": "Захищена нотатка" @@ -151,7 +151,7 @@ "message": "Код безпеки" }, "identityName": { - "message": "Назва" + "message": "Назва посвідчення" }, "company": { "message": "Компанія" @@ -346,7 +346,7 @@ "message": "Видалити" }, "nameRequired": { - "message": "Потрібна назва." + "message": "Необхідно ввести назву." }, "addedItem": { "message": "Запис додано" @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Додаткові налаштування Windows Hello" }, + "unlockWithPolkit": { + "message": "Розблокувати за допомогою системної автентифікації" + }, "windowsHelloConsentMessage": { "message": "Перевірити на Bitwarden." }, + "polkitConsentMessage": { + "message": "Автентифікуйтесь для розблокування Bitwarden." + }, "unlockWithTouchId": { "message": "Розблокувати з Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Запитувати Windows Hello під час запуску" }, + "autoPromptPolkit": { + "message": "Запитувати системну автентифікацію під час запуску" + }, "autoPromptTouchId": { "message": "Запитувати Touch ID під час запуску" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Для використання біометрії в браузері необхідно спершу налаштувати її в програмі на комп'ютері." }, + "biometricsManualSetupTitle": { + "message": "Автоналаштування недоступне" + }, + "biometricsManualSetupDesc": { + "message": "У зв'язку з методом встановлення, не можна автоматично ввімкнути підтримку біометрії. Бажаєте переглянути документацію про те, як зробити це вручну?" + }, "personalOwnershipSubmitError": { "message": "Згідно з політикою компанії, вам заборонено зберігати записи в особистому сховищі. Змініть опцію власника на організацію та виберіть серед доступних збірок." }, @@ -1814,7 +1829,7 @@ "message": "Політика організації впливає на ваші параметри власності." }, "personalOwnershipPolicyInEffectImports": { - "message": "Політика організації заблокувала імпортування елементів до вашого особистого сховища." + "message": "Політика організації заблокувала імпортування записів до вашого особистого сховища." }, "allSends": { "message": "Усі відправлення", @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш головний пароль не відповідає одній або більше політикам вашої організації. Щоб отримати доступ до сховища, вам необхідно оновити свій головний пароль зараз. Продовживши, ви вийдете з поточного сеансу, після чого потрібно буде повторно виконати вхід. Сеанси на інших пристроях можуть залишатися активними протягом однієї години." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша організація вимкнула шифрування довірених пристроїв. Встановіть головний пароль для доступу до сховища." + }, "tryAgain": { "message": "Спробуйте знову" }, @@ -2196,7 +2214,7 @@ "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Записи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", @@ -2782,7 +2800,7 @@ "message": "Дані успішно імпортовано" }, "importSuccessNumberOfItems": { - "message": "Всього імпортовано $AMOUNT$ елементів.", + "message": "Всього імпортовано $AMOUNT$ записів.", "placeholders": { "amount": { "content": "$1", @@ -2849,7 +2867,7 @@ } }, "importUnassignedItemsError": { - "message": "Файл містить непризначені елементи." + "message": "Файл містить непризначені записи." }, "selectFormat": { "message": "Оберіть формат імпортованого файлу" @@ -3025,5 +3043,11 @@ }, "data": { "message": "Дані" + }, + "fileSends": { + "message": "Відправлення файлів" + }, + "textSends": { + "message": "Відправлення тексту" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 9e5b6b5e36a..035c5bb845f 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "Cài đặt Windows Hello bổ sung" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Xác minh cho Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Mở khóa với Touch ID" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "Yêu cầu xác minh Windows Hello khi mở" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Yêu cầu xác minh Touch ID khi mở" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "Sinh trắc học trên trình duyệt yêu cầu sinh trắc học trên máy tính phải được cài đặt trước." }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "Do chính sách doanh nghiệp, bạn bị hạn chế lưu các mục vào kho cá nhân của mình. Thay đổi tùy chọn quyền sở hữu thành một tổ chức và chọn từ các bộ sưu tập có sẵn." }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "Mật khẩu chính của bạn không đáp ứng chính sách tổ chức của bạn. Để truy cập kho, bạn phải cập nhật mật khẩu chính của mình ngay bây giờ. Việc tiếp tục sẽ đăng xuất bạn khỏi phiên hiện tại và bắt buộc đăng nhập lại. Các phiên hoạt động trên các thiết bị khác có thể tiếp tục duy trì hoạt động trong tối đa một giờ." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "Thử lại" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Dữ liệu" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 36f4bad1a07..a715d09c9ac 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -157,7 +157,7 @@ "message": "公司" }, "ssn": { - "message": "社会保险号码" + "message": "社会保障号码" }, "passportNumber": { "message": "护照号码" @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "额外的 Windows Hello 设置" }, + "unlockWithPolkit": { + "message": "使用系统身份验证解锁" + }, "windowsHelloConsentMessage": { "message": "验证 Bitwarden。" }, + "polkitConsentMessage": { + "message": "验证以解锁 Bitwarden。" + }, "unlockWithTouchId": { "message": "使用触控 ID 解锁" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "应用程序启动时要求使用 Windows Hello" }, + "autoPromptPolkit": { + "message": "启动时请求系统身份验证" + }, "autoPromptTouchId": { "message": "应用程序启动时要求使用触控 ID" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "需要先在桌面应用程序的设置中设置生物识别,才能使用浏览器中的生物识别。" }, + "biometricsManualSetupTitle": { + "message": "自动设置不可用" + }, + "biometricsManualSetupDesc": { + "message": "由于安装方式的原因,生物识别支持无法自动启用。您想要打开关于如何手动执行此操作的文档吗?" + }, "personalOwnershipSubmitError": { "message": "由于某个企业策略,您不能将项目保存到您的个人密码库。将所有权选项更改为组织,并从可用的集合中选择。" }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "您的组织禁用了信任设备加密。要访问您的密码库,请设置一个主密码。" + }, "tryAgain": { "message": "请重试" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "数据" + }, + "fileSends": { + "message": "文件 Send" + }, + "textSends": { + "message": "文本 Send" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 33d7acd2340..96c358d1482 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1510,9 +1510,15 @@ "additionalWindowsHelloSettings": { "message": "額外的 Windows Hello 設定" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "驗證 Bitwarden。" }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "使用 Touch ID 解鎖" }, @@ -1525,6 +1531,9 @@ "autoPromptWindowsHello": { "message": "啟動時詢問 Windows Hello" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "啟動時詢問 Touch ID" }, @@ -1804,6 +1813,12 @@ "biometricsNotEnabledDesc": { "message": "需先在桌面應用程式的設定中啟用生物特徵辨識,才能使用瀏覽器的生物特徵辨識。" }, + "biometricsManualSetupTitle": { + "message": "Autometic setup not available" + }, + "biometricsManualSetupDesc": { + "message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?" + }, "personalOwnershipSubmitError": { "message": "由於某個企業原則,您被限制為儲存項目到您的個人密碼庫。將擁有權變更為組織,並從可用的集合中選擇。" }, @@ -2015,6 +2030,9 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "再試一次" }, @@ -3025,5 +3043,11 @@ }, "data": { "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index d3d99c4cfb3..59cbea3910c 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.7.2", + "version": "2024.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.7.2", + "version": "2024.8.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a6bd0d9ef39..bed3b631dac 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.7.2", + "version": "2024.8.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 10982e7270f..8a6a51f4c01 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -69,7 +69,7 @@ export class ElectronCryptoService extends CryptoService { await super.clearStoredUserKey(keySuffix, userId); } - protected override async storeAdditionalKeys(key: UserKey, userId?: UserId) { + protected override async storeAdditionalKeys(key: UserKey, userId: UserId) { await super.storeAdditionalKeys(key, userId); const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId); diff --git a/apps/web/package.json b/apps/web/package.json index f8ef4a8030c..35ef8056ee5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.7.3", + "version": "2024.8.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/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 8df770686f4..c12d133f37b 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 @@ -19,9 +19,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; 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 { ErrorResponse } from "@bitwarden/common/models/response/error.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -96,9 +94,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private organization$ = this.organizationService .get$(this.organizationId) .pipe(shareReplay({ refCount: true })); - private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( - FeatureFlag.FlexibleCollectionsV1, - ); protected PermissionMode = PermissionMode; protected ResultType = GroupAddEditDialogResultType; @@ -179,27 +174,19 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - protected allowAdminAccessToAllCollectionItems$ = combineLatest([ - this.organization$, - this.flexibleCollectionsV1Enabled$, - ]).pipe( - map(([organization, flexibleCollectionsV1Enabled]) => { - if (!flexibleCollectionsV1Enabled) { - return true; - } - + protected allowAdminAccessToAllCollectionItems$ = this.organization$.pipe( + map((organization) => { return organization.allowAdminAccessToAllCollectionItems; }), ); protected canAssignAccessToAnyCollection$ = combineLatest([ this.organization$, - this.flexibleCollectionsV1Enabled$, this.allowAdminAccessToAllCollectionItems$, ]).pipe( map( - ([org, flexibleCollectionsV1Enabled, allowAdminAccessToAllCollectionItems]) => - org.canEditAnyCollection(flexibleCollectionsV1Enabled) || + ([org, allowAdminAccessToAllCollectionItems]) => + org.canEditAnyCollection || // Manage Groups custom permission cannot edit any collection but they can assign access from this dialog // if permitted by collection management settings (org.permissions.manageGroups && allowAdminAccessToAllCollectionItems), @@ -224,7 +211,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private dialogService: DialogService, private organizationService: OrganizationService, - private configService: ConfigService, private accountService: AccountService, private collectionAdminService: CollectionAdminService, ) { @@ -242,27 +228,13 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.cannotAddSelfToGroup$, this.accountService.activeAccount$, this.organization$, - this.flexibleCollectionsV1Enabled$, ]) .pipe(takeUntil(this.destroy$)) .subscribe( - ([ - collections, - members, - group, - restrictGroupAccess, - activeAccount, - organization, - flexibleCollectionsV1Enabled, - ]) => { + ([collections, members, group, restrictGroupAccess, activeAccount, organization]) => { this.members = members; this.group = group; - this.collections = mapToAccessItemViews( - collections, - organization, - flexibleCollectionsV1Enabled, - group, - ); + this.collections = mapToAccessItemViews(collections, organization, group); if (this.group != undefined) { // Must detect changes so that AccessSelector @Inputs() are aware of the latest @@ -384,7 +356,6 @@ function mapToAccessSelections(group: GroupView, items: AccessItemView[]): Acces function mapToAccessItemViews( collections: CollectionAdminView[], organization: Organization, - flexibleCollectionsV1Enabled: boolean, group?: GroupView, ): AccessItemView[] { return ( @@ -396,7 +367,7 @@ function mapToAccessItemViews( type: AccessItemType.Collection, labelName: c.name, listName: c.name, - readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled), + readonly: !c.canEditGroupAccess(organization), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, }; }) 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 81830d12138..ef36f2b80ba 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 @@ -23,8 +23,6 @@ import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permi 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"; -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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -143,7 +141,6 @@ export class MemberDialogComponent implements OnDestroy { private userService: UserAdminService, private organizationUserService: OrganizationUserService, private dialogService: DialogService, - private configService: ConfigService, private accountService: AccountService, organizationService: OrganizationService, ) { @@ -174,15 +171,8 @@ export class MemberDialogComponent implements OnDestroy { ? this.userService.get(this.params.organizationId, this.params.organizationUserId) : of(null); - this.allowAdminAccessToAllCollectionItems$ = combineLatest([ - this.organization$, - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), - ]).pipe( - map(([organization, flexibleCollectionsV1Enabled]) => { - if (!flexibleCollectionsV1Enabled) { - return true; - } - + this.allowAdminAccessToAllCollectionItems$ = this.organization$.pipe( + map((organization) => { return organization.allowAdminAccessToAllCollectionItems; }), ); @@ -208,18 +198,13 @@ export class MemberDialogComponent implements OnDestroy { } }); - const flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( - FeatureFlag.FlexibleCollectionsV1, - ); - this.canAssignAccessToAnyCollection$ = combineLatest([ this.organization$, - flexibleCollectionsV1Enabled$, this.allowAdminAccessToAllCollectionItems$, ]).pipe( map( - ([org, flexibleCollectionsV1Enabled, allowAdminAccessToAllCollectionItems]) => - org.canEditAnyCollection(flexibleCollectionsV1Enabled) || + ([org, allowAdminAccessToAllCollectionItems]) => + org.canEditAnyCollection || // Manage Users custom permission cannot edit any collection but they can assign access from this dialog // if permitted by collection management settings (org.permissions.manageUsers && allowAdminAccessToAllCollectionItems), @@ -231,49 +216,39 @@ export class MemberDialogComponent implements OnDestroy { collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: userDetails$, groups: groups$, - flexibleCollectionsV1Enabled: flexibleCollectionsV1Enabled$, }) .pipe(takeUntil(this.destroy$)) - .subscribe( - ({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => { - this.setFormValidators(organization); + .subscribe(({ organization, collections, userDetails, groups }) => { + this.setFormValidators(organization); - // Groups tab: populate available groups - this.groupAccessItems = [].concat( - groups.map((g) => mapGroupToAccessItemView(g)), + // Groups tab: populate available groups + this.groupAccessItems = [].concat( + groups.map((g) => mapGroupToAccessItemView(g)), + ); + + // Collections tab: Populate all available collections (including current user access where applicable) + this.collectionAccessItems = collections + .map((c) => + mapCollectionToAccessItemView( + c, + organization, + userDetails == null + ? undefined + : c.users.find((access) => access.id === userDetails.id), + ), + ) + // But remove collections that we can't assign access to, unless the user is already assigned + .filter( + (item) => + !item.readonly || userDetails?.collections.some((access) => access.id == item.id), ); - // Collections tab: Populate all available collections (including current user access where applicable) - this.collectionAccessItems = collections - .map((c) => - mapCollectionToAccessItemView( - c, - organization, - flexibleCollectionsV1Enabled, - userDetails == null - ? undefined - : c.users.find((access) => access.id === userDetails.id), - ), - ) - // But remove collections that we can't assign access to, unless the user is already assigned - .filter( - (item) => - !item.readonly || userDetails?.collections.some((access) => access.id == item.id), - ); + if (userDetails != null) { + this.loadOrganizationUser(userDetails, groups, collections, organization); + } - if (userDetails != null) { - this.loadOrganizationUser( - userDetails, - groups, - collections, - organization, - flexibleCollectionsV1Enabled, - ); - } - - this.loading = false; - }, - ); + this.loading = false; + }); } private setFormValidators(organization: Organization) { @@ -297,7 +272,6 @@ export class MemberDialogComponent implements OnDestroy { groups: GroupView[], collections: CollectionAdminView[], organization: Organization, - flexibleCollectionsV1Enabled: boolean, ) { if (!userDetails) { throw new Error("Could not find user to edit."); @@ -341,13 +315,7 @@ export class MemberDialogComponent implements OnDestroy { // Populate additional collection access via groups (rendered as separate rows from user access) this.collectionAccessItems = this.collectionAccessItems.concat( collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView( - collection, - organization, - flexibleCollectionsV1Enabled, - accessSelection, - group, - ), + mapCollectionToAccessItemView(collection, organization, accessSelection, group), ), ); @@ -621,7 +589,6 @@ export class MemberDialogComponent implements OnDestroy { function mapCollectionToAccessItemView( collection: CollectionAdminView, organization: Organization, - flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, group?: GroupView, ): AccessItemView { @@ -630,9 +597,7 @@ function mapCollectionToAccessItemView( id: group ? `${collection.id}-${group.id}` : collection.id, labelName: collection.name, listName: collection.name, - readonly: - group !== undefined || - !collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled), + readonly: group !== undefined || !collection.canEditUserAccess(organization), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index f453546fcad..af605dfd273 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -56,7 +56,7 @@ >

    {{ "collectionManagement" | i18n }}

    {{ "collectionManagementDesc" | i18n }}

    - + {{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index c53a4991d5e..7cfbee166e3 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -10,8 +10,6 @@ import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -40,10 +38,6 @@ export class AccountComponent implements OnInit, OnDestroy { org: OrganizationResponse; taxFormPromise: Promise; - flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( - FeatureFlag.FlexibleCollectionsV1, - ); - // FormGroup validators taken from server Organization domain object protected formGroup = this.formBuilder.group({ orgName: this.formBuilder.control( @@ -83,7 +77,6 @@ export class AccountComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private formBuilder: FormBuilder, - private configService: ConfigService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index b05c82226ab..be3bd0860f1 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -94,7 +94,7 @@ export class AppComponent implements OnDestroy, OnInit { private policyService: InternalPolicyService, protected policyListService: PolicyListService, private keyConnectorService: KeyConnectorService, - private configService: ConfigService, + protected configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.html b/apps/web/src/app/auth/settings/two-factor-duo.component.html index 6c733ed798a..f20bd4f5f70 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.html +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.html @@ -25,13 +25,7 @@ {{ "twoFactorDuoClientSecret" | i18n }} - + {{ "twoFactorDuoApiHostname" | i18n }} diff --git a/apps/web/src/app/auth/two-factor.component.html b/apps/web/src/app/auth/two-factor.component.html index b09ab68c312..a941413dbb0 100644 --- a/apps/web/src/app/auth/two-factor.component.html +++ b/apps/web/src/app/auth/two-factor.component.html @@ -37,14 +37,7 @@ {{ "verificationCode" | i18n }} - +
    diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index 7b1e46805bd..b43d3cef342 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -65,9 +65,11 @@ export class PremiumComponent implements OnInit { } } submit = async () => { - if (!this.taxInfoComponent?.taxFormGroup.valid && this.taxInfoComponent?.taxFormGroup.touched) { - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - return; + if (this.taxInfoComponent) { + if (!this.taxInfoComponent?.taxFormGroup.valid) { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + return; + } } this.licenseForm.markAllAsTouched(); this.addonForm.markAllAsTouched(); diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.html b/apps/web/src/app/billing/organizations/adjust-subscription.component.html index 9fe8d205407..61197e86d22 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.html +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.html @@ -5,8 +5,9 @@ {{ "subscriptionSeats" | i18n }} - {{ "total" | i18n }}: {{ additionalSeatCount || 0 }} × - {{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} / + {{ "total" | i18n }}: + {{ adjustSubscriptionForm.value.newSeatCount || 0 }} × + {{ seatPrice | currency: "$" }} = {{ seatTotalCost | currency: "$" }} / {{ interval | i18n }} @@ -43,7 +44,8 @@ step="1" /> - {{ "maxSeatCost" | i18n }}: {{ additionalMaxSeatCount || 0 }} × + {{ "maxSeatCost" | i18n }}: + {{ adjustSubscriptionForm.value.newMaxSeats || 0 }} × {{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} / {{ interval | i18n }} diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index b843c79cb95..c98a6b97c41 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -99,11 +99,12 @@ export class AdjustSubscription implements OnInit, OnDestroy { : 0; } - get adjustedSeatTotal(): number { - return this.additionalSeatCount * this.seatPrice; + get maxSeatTotal(): number { + return Math.abs((this.adjustSubscriptionForm.value.newMaxSeats ?? 0) * this.seatPrice); } - get maxSeatTotal(): number { - return this.additionalMaxSeatCount * this.seatPrice; + get seatTotalCost(): number { + const totalSeat = Math.abs(this.adjustSubscriptionForm.value.newSeatCount * this.seatPrice); + return totalSeat; } } diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 13b15eb9ee1..995dcb23890 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -554,9 +554,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } submit = async () => { - if (!this.taxComponent?.taxFormGroup.valid && this.taxComponent?.taxFormGroup.touched) { - this.taxComponent?.taxFormGroup.markAllAsTouched(); - return; + if (this.taxComponent) { + if (!this.taxComponent?.taxFormGroup.valid) { + this.taxComponent?.taxFormGroup.markAllAsTouched(); + return; + } } if (this.singleOrgPolicyBlock) { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 4d8acbde0d1..3b7ba0a727a 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -49,6 +49,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; /* eslint-enable import/no-restricted-paths -- Implementation for memory storage */ +import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { DefaultThemeStateService, ThemeStateService, @@ -63,7 +64,6 @@ import { I18nService } from "../core/i18n.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; -import { WindowStorageService } from "../platform/window-storage.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; import { EventService } from "./event.service"; diff --git a/apps/web/src/app/platform/web-migration-runner.spec.ts b/apps/web/src/app/platform/web-migration-runner.spec.ts index 4b2949230e4..c27be4a145e 100644 --- a/apps/web/src/app/platform/web-migration-runner.spec.ts +++ b/apps/web/src/app/platform/web-migration-runner.spec.ts @@ -3,11 +3,11 @@ import { MockProxy, mock } from "jest-mock-extended"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; +import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { MigrationBuilder } from "@bitwarden/common/state-migrations/migration-builder"; import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper"; import { WebMigrationRunner } from "./web-migration-runner"; -import { WindowStorageService } from "./window-storage.service"; describe("WebMigrationRunner", () => { let logService: MockProxy; diff --git a/apps/web/src/app/platform/web-migration-runner.ts b/apps/web/src/app/platform/web-migration-runner.ts index 392eeeae045..7ac10cd2e08 100644 --- a/apps/web/src/app/platform/web-migration-runner.ts +++ b/apps/web/src/app/platform/web-migration-runner.ts @@ -4,10 +4,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper"; -import { WindowStorageService } from "./window-storage.service"; - export class WebMigrationRunner extends MigrationRunner { constructor( diskStorage: AbstractStorageService, diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index 0640681cf44..fbe0649c7aa 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -80,13 +80,9 @@ {{ "grantCollectionAccessMembersOnly" | i18n }} - {{ " " + ("adminCollectionAccess" | i18n) }} + + {{ " " + ("adminCollectionAccess" | i18n) }} +

    (); protected organizations$: Observable; @@ -113,7 +107,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private organizationUserService: OrganizationUserService, - private configService: ConfigService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, ) { @@ -163,95 +156,90 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { groups: groups$, // Collection(s) needed to map readonlypermission for (potential) access selector disabled state users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }), - flexibleCollectionsV1: this.flexibleCollectionsV1Enabled$, }) .pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$)) - .subscribe( - ({ organization, collections: allCollections, groups, users, flexibleCollectionsV1 }) => { - this.organization = organization; - this.accessItems = [].concat( - groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)), - users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)), - ); + .subscribe(({ organization, collections: allCollections, groups, users }) => { + this.organization = organization; + this.accessItems = [].concat( + groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)), + users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)), + ); - // Force change detection to update the access selector's items - this.changeDetectorRef.detectChanges(); + // Force change detection to update the access selector's items + this.changeDetectorRef.detectChanges(); - this.nestOptions = this.params.limitNestedCollections - ? allCollections.filter((c) => c.manage) - : allCollections; + this.nestOptions = this.params.limitNestedCollections + ? allCollections.filter((c) => c.manage) + : allCollections; - if (this.params.collectionId) { - this.collection = allCollections.find((c) => c.id === this.collectionId); - // Ensure we don't allow nesting the current collection within itself - this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId); + if (this.params.collectionId) { + this.collection = allCollections.find((c) => c.id === this.collectionId); + // Ensure we don't allow nesting the current collection within itself + this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId); - if (!this.collection) { - throw new Error("Could not find collection to edit."); - } - - // Parse the name to find its parent name - const { name, parent: parentName } = parseName(this.collection); - - // Determine if the user can see/select the parent collection - if (parentName !== undefined) { - if ( - this.organization.canViewAllCollections && - !allCollections.find((c) => c.name === parentName) - ) { - // The user can view all collections, but the parent was not found -> assume it has been deleted - this.deletedParentName = parentName; - } else if (!this.nestOptions.find((c) => c.name === parentName)) { - // We cannot find the current parent collection in our list of options, so add a placeholder - this.nestOptions.unshift({ name: parentName } as CollectionView); - } - } - - const accessSelections = mapToAccessSelections(this.collection); - this.formGroup.patchValue({ - name, - externalId: this.collection.externalId, - parent: parentName, - access: accessSelections, - }); - this.showDeleteButton = - !this.dialogReadonly && - this.collection.canDelete(organization, flexibleCollectionsV1); - } else { - const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId); - const currentOrgUserId = users.data.find( - (u) => u.userId === this.organization?.userId, - )?.id; - const initialSelection: AccessItemValue[] = - currentOrgUserId !== undefined - ? [ - { - id: currentOrgUserId, - type: AccessItemType.Member, - permission: CollectionPermission.Manage, - }, - ] - : []; - - this.formGroup.patchValue({ - parent: parent?.name ?? undefined, - access: initialSelection, - }); + if (!this.collection) { + throw new Error("Could not find collection to edit."); } - if (flexibleCollectionsV1 && !organization.allowAdminAccessToAllCollectionItems) { - this.formGroup.controls.access.addValidators(validateCanManagePermission); - } else { - this.formGroup.controls.access.removeValidators(validateCanManagePermission); + // Parse the name to find its parent name + const { name, parent: parentName } = parseName(this.collection); + + // Determine if the user can see/select the parent collection + if (parentName !== undefined) { + if ( + this.organization.canViewAllCollections && + !allCollections.find((c) => c.name === parentName) + ) { + // The user can view all collections, but the parent was not found -> assume it has been deleted + this.deletedParentName = parentName; + } else if (!this.nestOptions.find((c) => c.name === parentName)) { + // We cannot find the current parent collection in our list of options, so add a placeholder + this.nestOptions.unshift({ name: parentName } as CollectionView); + } } - this.formGroup.controls.access.updateValueAndValidity(); - this.handleFormGroupReadonly(this.dialogReadonly); + const accessSelections = mapToAccessSelections(this.collection); + this.formGroup.patchValue({ + name, + externalId: this.collection.externalId, + parent: parentName, + access: accessSelections, + }); + this.showDeleteButton = !this.dialogReadonly && this.collection.canDelete(organization); + } else { + const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId); + const currentOrgUserId = users.data.find( + (u) => u.userId === this.organization?.userId, + )?.id; + const initialSelection: AccessItemValue[] = + currentOrgUserId !== undefined + ? [ + { + id: currentOrgUserId, + type: AccessItemType.Member, + permission: CollectionPermission.Manage, + }, + ] + : []; - this.loading = false; - this.showAddAccessWarning = this.handleAddAccessWarning(flexibleCollectionsV1); - }, - ); + this.formGroup.patchValue({ + parent: parent?.name ?? undefined, + access: initialSelection, + }); + } + + if (!organization.allowAdminAccessToAllCollectionItems) { + this.formGroup.controls.access.addValidators(validateCanManagePermission); + } else { + this.formGroup.controls.access.removeValidators(validateCanManagePermission); + } + this.formGroup.controls.access.updateValueAndValidity(); + + this.handleFormGroupReadonly(this.dialogReadonly); + + this.loading = false; + this.showAddAccessWarning = this.handleAddAccessWarning(); + }); } protected get collectionId() { @@ -361,9 +349,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - private handleAddAccessWarning(flexibleCollectionsV1: boolean): boolean { + private handleAddAccessWarning(): boolean { if ( - flexibleCollectionsV1 && !this.organization?.allowAdminAccessToAllCollectionItems && this.params.isAddAccessCollection ) { 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 873bdd3e1a0..b5f910cd1a0 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 @@ -34,7 +34,6 @@ export class VaultCollectionRowComponent { @Input() organizations: Organization[]; @Input() groups: GroupView[]; @Input() showPermissionsColumn: boolean; - @Input() flexibleCollectionsV1Enabled: boolean; @Input() restrictProviderAccess: boolean; @Output() onEvent = new EventEmitter(); @@ -57,10 +56,6 @@ export class VaultCollectionRowComponent { } get showAddAccess() { - if (!this.flexibleCollectionsV1Enabled) { - return false; - } - if (this.collection.id == Unassigned) { return false; } @@ -71,7 +66,7 @@ export class VaultCollectionRowComponent { return ( !this.organization?.allowAdminAccessToAllCollectionItems && this.collection.unmanaged && - this.organization?.canEditUnmanagedCollections() + this.organization?.canEditUnmanagedCollections ); } @@ -114,10 +109,6 @@ export class VaultCollectionRowComponent { } protected get showCheckbox() { - if (this.flexibleCollectionsV1Enabled) { - return this.collection?.id !== Unassigned; - } - - return this.canDeleteCollection; + return this.collection?.id !== Unassigned; } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index a4f41d25078..2f294a758db 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -113,7 +113,6 @@ [canDeleteCollection]="canDeleteCollection(item.collection)" [canEditCollection]="canEditCollection(item.collection)" [canViewCollectionInfo]="canViewCollectionInfo(item.collection)" - [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" [restrictProviderAccess]="restrictProviderAccess" [checked]="selection.isSelected(item)" (checkedToggled)="selection.toggle(item)" 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 bfb30f3f769..2709091b0c8 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 @@ -44,7 +44,6 @@ export class VaultItemsComponent { @Input() showBulkAddToCollections = false; @Input() showPermissionsColumn = false; @Input() viewingOrgVault: boolean; - @Input({ required: true }) flexibleCollectionsV1Enabled = false; @Input() addAccessStatus: number; @Input() addAccessToggle: boolean; @Input() restrictProviderAccess: boolean; @@ -120,7 +119,7 @@ export class VaultItemsComponent { const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); - return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); + return collection.canEdit(organization); } protected canDeleteCollection(collection: CollectionView): boolean { @@ -131,12 +130,12 @@ export class VaultItemsComponent { const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); - return collection.canDelete(organization, this.flexibleCollectionsV1Enabled); + return collection.canDelete(organization); } protected canViewCollectionInfo(collection: CollectionView) { const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); - return collection.canViewCollectionInfo(organization, this.flexibleCollectionsV1Enabled); + return collection.canViewCollectionInfo(organization); } protected toggleAll() { @@ -214,11 +213,7 @@ export class VaultItemsComponent { const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); return ( - (organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) && - this.viewingOrgVault) || + (organization.canEditAllCiphers(this.restrictProviderAccess) && this.viewingOrgVault) || cipher.edit ); } @@ -230,21 +225,12 @@ export class VaultItemsComponent { this.selection.clear(); - if (this.flexibleCollectionsV1Enabled) { - // Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission - this.editableItems = items.filter( - (item) => - item.cipher !== undefined || - (item.collection !== undefined && item.collection.id !== Unassigned), - ); - } else { - // only collections the user can delete are selectable - this.editableItems = items.filter( - (item) => - item.cipher !== undefined || - (item.collection !== undefined && this.canDeleteCollection(item.collection)), - ); - } + // Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission + this.editableItems = items.filter( + (item) => + item.cipher !== undefined || + (item.collection !== undefined && item.collection.id !== Unassigned), + ); this.dataSource.data = items; } @@ -293,10 +279,7 @@ export class VaultItemsComponent { const organization = this.allOrganizations.find((o) => o.id === orgId); const canEditOrManageAllCiphers = - organization?.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) && this.viewingOrgVault; + organization?.canEditAllCiphers(this.restrictProviderAccess) && this.viewingOrgVault; const collectionNotSelected = this.selection.selected.filter((item) => item.collection).length === 0; @@ -317,9 +300,7 @@ export class VaultItemsComponent { const canEditOrManageAllCiphers = organizations.length > 0 && - organizations.every((org) => - org?.canEditAllCiphers(this.flexibleCollectionsV1Enabled, this.restrictProviderAccess), - ); + organizations.every((org) => org?.canEditAllCiphers(this.restrictProviderAccess)); const canDeleteCollections = this.selection.selected .filter((item) => item.collection) diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 6e842023d33..10f894505c9 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -41,61 +41,44 @@ export class CollectionAdminView extends CollectionView { /** * Returns true if the user can edit a collection (including user and group access) from the Admin Console. */ - override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { + override canEdit(org: Organization): boolean { return ( - org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || - (flexibleCollectionsV1Enabled && this.unmanaged && org?.canEditUnmanagedCollections()) || - super.canEdit(org, flexibleCollectionsV1Enabled) + org?.canEditAnyCollection || + (this.unmanaged && org?.canEditUnmanagedCollections) || + super.canEdit(org) ); } /** * Returns true if the user can delete a collection from the Admin Console. */ - override canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { - return ( - org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) || - super.canDelete(org, flexibleCollectionsV1Enabled) - ); + override canDelete(org: Organization): boolean { + return org?.canDeleteAnyCollection || super.canDelete(org); } /** * Whether the user can modify user access to this collection */ - canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { - const allowAdminAccessToAllCollectionItems = - !flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems; - + canEditUserAccess(org: Organization): boolean { return ( - (org.permissions.manageUsers && allowAdminAccessToAllCollectionItems) || - this.canEdit(org, flexibleCollectionsV1Enabled) + (org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org) ); } /** * Whether the user can modify group access to this collection */ - canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { - const allowAdminAccessToAllCollectionItems = - !flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems; - + canEditGroupAccess(org: Organization): boolean { return ( - (org.permissions.manageGroups && allowAdminAccessToAllCollectionItems) || - this.canEdit(org, flexibleCollectionsV1Enabled) + (org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) || + this.canEdit(org) ); } /** * Returns true if the user can view collection info and access in a read-only state from the Admin Console */ - override canViewCollectionInfo( - org: Organization | undefined, - flexibleCollectionsV1Enabled: boolean, - ): boolean { - if (!flexibleCollectionsV1Enabled) { - return false; - } - + override canViewCollectionInfo(org: Organization | undefined): boolean { if (this.isUnassignedCollection) { return false; } 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 c0de8c6bd22..617628a0b37 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -54,10 +54,6 @@ export class BulkDeleteDialogComponent { collections: CollectionView[]; unassignedCiphers: string[]; - private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( - FeatureFlag.FlexibleCollectionsV1, - ); - private restrictProviderAccess$ = this.configService.getFeatureFlag$( FeatureFlag.RestrictProviderAccess, ); @@ -96,13 +92,9 @@ export class BulkDeleteDialogComponent { deletePromises.push(this.deleteCiphersAdmin(this.unassignedCiphers)); } if (this.cipherIds.length) { - const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); - if ( - !this.organization || - !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess) - ) { + if (!this.organization || !this.organization.canEditAllCiphers(restrictProviderAccess)) { deletePromises.push(this.deleteCiphers()); } else { deletePromises.push(this.deleteCiphersAdmin(this.cipherIds)); @@ -134,12 +126,8 @@ export class BulkDeleteDialogComponent { }; private async deleteCiphers(): Promise { - const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); - const asAdmin = this.organization?.canEditAllCiphers( - flexibleCollectionsV1Enabled, - restrictProviderAccess, - ); + const asAdmin = this.organization?.canEditAllCiphers(restrictProviderAccess); if (this.permanent) { await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); } else { @@ -157,12 +145,9 @@ export class BulkDeleteDialogComponent { } private async deleteCollections(): Promise { - const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); // From org vault if (this.organization) { - if ( - this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsV1Enabled)) - ) { + if (this.collections.some((c) => !c.canDelete(this.organization))) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), @@ -179,7 +164,7 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; for (const organization of this.organizations) { const orgCollections = this.collections.filter((o) => o.organizationId === organization.id); - if (orgCollections.some((c) => !c.canDelete(organization, flexibleCollectionsV1Enabled))) { + if (orgCollections.some((c) => !c.canDelete(organization))) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index d9c2145f0b5..e4029ef8669 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -32,13 +32,7 @@ [(ngModel)]="$any(c).checked" name="Collection[{{ i }}].Checked" appStopProp - [disabled]=" - !c.canEditItems( - this.organization, - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess - ) - " + [disabled]="!c.canEditItems(this.organization, this.restrictProviderAccess)" /> {{ c.name }} diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index af9c3476bd5..9795f879776 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -50,13 +50,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On } check(c: CollectionView, select?: boolean) { - if ( - !c.canEditItems( - this.organization, - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!c.canEditItems(this.organization, this.restrictProviderAccess)) { return; } (c as any).checked = select == null ? !(c as any).checked : select; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index e5938b51979..63bf3d5e4c2 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -36,6 +36,8 @@ describe("vault filter service", () => { let organizations: ReplaySubject; let folderViews: ReplaySubject; let collectionViews: ReplaySubject; + let personalOwnershipPolicy: ReplaySubject; + let singleOrgPolicy: ReplaySubject; let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -56,10 +58,18 @@ describe("vault filter service", () => { organizations = new ReplaySubject(1); folderViews = new ReplaySubject(1); collectionViews = new ReplaySubject(1); + personalOwnershipPolicy = new ReplaySubject(1); + singleOrgPolicy = new ReplaySubject(1); organizationService.memberOrganizations$ = organizations; folderService.folderViews$ = folderViews; collectionService.decryptedCollections$ = collectionViews; + policyService.policyAppliesToActiveUser$ + .calledWith(PolicyType.PersonalOwnership) + .mockReturnValue(personalOwnershipPolicy); + policyService.policyAppliesToActiveUser$ + .calledWith(PolicyType.SingleOrg) + .mockReturnValue(singleOrgPolicy); vaultFilterService = new VaultFilterService( organizationService, @@ -100,6 +110,8 @@ describe("vault filter service", () => { beforeEach(() => { const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")]; organizations.next(storedOrgs); + personalOwnershipPolicy.next(false); + singleOrgPolicy.next(false); }); it("returns a nested tree", async () => { @@ -111,9 +123,7 @@ describe("vault filter service", () => { }); it("hides My Vault if personal ownership policy is enabled", async () => { - policyService.policyAppliesToUser - .calledWith(PolicyType.PersonalOwnership) - .mockResolvedValue(true); + personalOwnershipPolicy.next(true); const tree = await firstValueFrom(vaultFilterService.organizationTree$); @@ -122,7 +132,7 @@ describe("vault filter service", () => { }); it("returns 1 organization and My Vault if single organization policy is enabled", async () => { - policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true); + singleOrgPolicy.next(true); const tree = await firstValueFrom(vaultFilterService.organizationTree$); @@ -132,10 +142,8 @@ describe("vault filter service", () => { }); it("returns 1 organization if both single organization and personal ownership policies are enabled", async () => { - policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true); - policyService.policyAppliesToUser - .calledWith(PolicyType.PersonalOwnership) - .mockResolvedValue(true); + singleOrgPolicy.next(true); + personalOwnershipPolicy.next(true); const tree = await firstValueFrom(vaultFilterService.organizationTree$); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 36cde762a00..ac20f86d0ee 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, + combineLatest, combineLatestWith, firstValueFrom, map, @@ -39,10 +40,15 @@ const NestingDelimiter = "/"; @Injectable() export class VaultFilterService implements VaultFilterServiceAbstraction { - organizationTree$: Observable> = - this.organizationService.memberOrganizations$.pipe( - switchMap((orgs) => this.buildOrganizationTree(orgs)), - ); + organizationTree$: Observable> = combineLatest([ + this.organizationService.memberOrganizations$, + this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg), + this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + ]).pipe( + switchMap(([orgs, singleOrgPolicy, personalOwnershipPolicy]) => + this.buildOrganizationTree(orgs, singleOrgPolicy, personalOwnershipPolicy), + ), + ); protected _organizationFilter = new BehaviorSubject(null); @@ -125,14 +131,16 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { } protected async buildOrganizationTree( - orgs?: Organization[], + orgs: Organization[], + singleOrgPolicy: boolean, + personalOwnershipPolicy: boolean, ): Promise> { const headNode = this.getOrganizationFilterHead(); - if (!(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))) { + if (!personalOwnershipPolicy) { const myVaultNode = this.getOrganizationFilterMyVault(); headNode.children.push(myVaultNode); } - if (await this.policyService.policyAppliesToUser(PolicyType.SingleOrg)) { + if (singleOrgPolicy) { orgs = orgs.slice(0, 1); } if (orgs) { diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html index b0ae308ea86..3f46cb803cf 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html @@ -69,30 +69,84 @@
    - - - - - - + + + + + + + + + + + + + + + + + + + + + +
    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 7803b1c32f2..403dbd2f675 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 @@ -14,6 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga 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 { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BreadcrumbsModule, MenuModule } from "@bitwarden/components"; @@ -47,6 +48,8 @@ export class VaultHeaderComponent implements OnInit { protected Unassigned = Unassigned; protected All = All; protected CollectionDialogTabType = CollectionDialogTabType; + protected CipherType = CipherType; + protected extensionRefreshEnabled = false; /** * Boolean to determine the loading state of the header. @@ -67,7 +70,7 @@ export class VaultHeaderComponent implements OnInit { @Input() canCreateCollections: boolean; /** Emits an event when the new item button is clicked in the header */ - @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); /** Emits an event when the new collection button is clicked in the 'New' dropdown menu */ @Output() onAddCollection = new EventEmitter(); @@ -81,16 +84,14 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); - private flexibleCollectionsV1Enabled = false; - constructor( private i18nService: I18nService, private configService: ConfigService, ) {} async ngOnInit() { - this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + this.extensionRefreshEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), ); } @@ -174,7 +175,7 @@ export class VaultHeaderComponent implements OnInit { const organization = this.organizations.find( (o) => o.id === this.collection?.node.organizationId, ); - return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled); + return this.collection.node.canEdit(organization); } async editCollection(tab: CollectionDialogTabType): Promise { @@ -192,15 +193,15 @@ export class VaultHeaderComponent implements OnInit { (o) => o.id === this.collection?.node.organizationId, ); - return this.collection.node.canDelete(organization, this.flexibleCollectionsV1Enabled); + return this.collection.node.canDelete(organization); } deleteCollection() { this.onDeleteCollection.emit(); } - protected addCipher() { - this.onAddCipher.emit(); + protected addCipher(cipherType?: CipherType) { + this.onAddCipher.emit(cipherType); } async addFolder(): Promise { diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html index 9f6f589df63..b9647e3237d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html @@ -22,7 +22,12 @@

    {{ "onboardingImportDataDetailsPartOne" | i18n }} {{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }} diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 490c07d7538..778132676fa 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -5,9 +5,11 @@ import { Subject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +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 { StateProvider } from "@bitwarden/common/platform/state"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum"; import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service"; @@ -24,6 +26,7 @@ describe("VaultOnboardingComponent", () => { let mockStateProvider: Partial; let setInstallExtLinkSpy: any; let individualVaultPolicyCheckSpy: any; + let mockConfigService: MockProxy; beforeEach(() => { mockPolicyService = mock(); @@ -42,6 +45,7 @@ describe("VaultOnboardingComponent", () => { }), ), }; + mockConfigService = mock(); // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -54,6 +58,7 @@ describe("VaultOnboardingComponent", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ApiService, useValue: mockApiService }, { provide: StateProvider, useValue: mockStateProvider }, + { provide: ConfigService, useValue: mockConfigService }, ], }).compileComponents(); fixture = TestBed.createComponent(VaultOnboardingComponent); @@ -178,4 +183,14 @@ describe("VaultOnboardingComponent", () => { expect(saveCompletedTasksSpy).toHaveBeenCalled(); }); }); + + describe("emitToAddCipher", () => { + it("always emits the `CipherType.Login` type when called", () => { + const emitSpy = jest.spyOn(component.onAddCipher, "emit"); + + component.emitToAddCipher(); + + expect(emitSpy).toHaveBeenCalledWith(CipherType.Login); + }); + }); }); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index a7331c73151..94ae1a4df47 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -16,7 +16,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LinkModule } from "@bitwarden/components"; @@ -41,7 +44,7 @@ import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-o export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { @Input() ciphers: CipherView[]; @Input() orgs: Organization[]; - @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); extensionUrl: string; isIndividualPolicyVault: boolean; @@ -53,12 +56,14 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { protected onboardingTasks$: Observable; protected showOnboarding = false; + protected extensionRefreshEnabled = false; constructor( protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, private apiService: ApiService, private vaultOnboardingService: VaultOnboardingServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { @@ -67,6 +72,9 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { this.setInstallExtLink(); this.individualVaultPolicyCheck(); this.checkForBrowserExtension(); + this.extensionRefreshEnabled = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); } async ngOnChanges(changes: SimpleChanges) { @@ -162,7 +170,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { } emitToAddCipher() { - this.onAddCipher.emit(); + this.onAddCipher.emit(CipherType.Login); } setInstallExtLink() { 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 f0be76018f7..b19a4509c15 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -6,14 +6,18 @@ [organizations]="allOrganizations" [canCreateCollections]="canCreateCollections" [collection]="selectedCollection" - (onAddCipher)="addCipher()" + (onAddCipher)="addCipher($event)" (onAddCollection)="addCollection()" (onAddFolder)="addFolder()" (onEditCollection)="editCollection(selectedCollection.node, $event.tab)" (onDeleteCollection)="deleteCollection(selectedCollection.node)" > - +

    @@ -52,7 +56,6 @@ [showAdminActions]="false" [showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async" (onEvent)="onVaultItemsEvent($event)" - [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" [vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async" > @@ -80,7 +83,7 @@ (click)="addCipher()" *ngIf="filter.type !== 'trash'" > - + {{ "newItem" | 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 6aca5662e53..77fd63a65f7 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -50,6 +50,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -157,13 +158,9 @@ export class VaultComponent implements OnInit, OnDestroy { protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable; - protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( - FeatureFlag.FlexibleCollectionsV1, - ); protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.VaultBulkManagementAction, ); - private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -552,7 +549,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async shareCipher(cipher: CipherView) { - if ((await this.flexibleCollectionsV1Enabled()) && cipher.organizationId != null) { + if (cipher.organizationId != null) { // You cannot move ciphers between organizations this.showMissingPermissionsError(); return; @@ -586,21 +583,27 @@ export class VaultComponent implements OnInit, OnDestroy { } } - async addCipher() { + async addCipher(cipherType?: CipherType) { const component = await this.editCipher(null); - component.type = this.activeFilter.cipherType; - if (this.activeFilter.organizationId !== "MyVault") { + component.type = cipherType || this.activeFilter.cipherType; + if ( + this.activeFilter.organizationId !== "MyVault" && + this.activeFilter.organizationId != null + ) { component.organizationId = this.activeFilter.organizationId; component.collections = ( await firstValueFrom(this.vaultFilterService.filteredCollections$) ).filter((c) => !c.readOnly && c.id != null); } const selectedColId = this.activeFilter.collectionId; - if (selectedColId !== "AllCollections") { - component.organizationId = component.collections.find( - (collection) => collection.id === selectedColId, - )?.organizationId; - component.collectionIds = [selectedColId]; + if (selectedColId !== "AllCollections" && selectedColId != null) { + const selectedCollection = ( + await firstValueFrom(this.vaultFilterService.filteredCollections$) + ).find((c) => c.id === selectedColId); + component.organizationId = selectedCollection?.organizationId; + if (!selectedCollection.readOnly) { + component.collectionIds = [selectedColId]; + } } component.folderId = this.activeFilter.folderId; } @@ -712,8 +715,7 @@ export class VaultComponent implements OnInit, OnDestroy { async deleteCollection(collection: CollectionView): Promise { const organization = await this.organizationService.get(collection.organizationId); - const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); - if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) { + if (!collection.canDelete(organization)) { this.showMissingPermissionsError(); return; } @@ -811,7 +813,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) { + if (!c.edit) { this.showMissingPermissionsError(); return; } @@ -834,7 +836,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async bulkRestore(ciphers: CipherView[]) { - if ((await this.flexibleCollectionsV1Enabled()) && ciphers.some((c) => !c.edit)) { + if (ciphers.some((c) => !c.edit)) { this.showMissingPermissionsError(); return; } @@ -887,7 +889,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) { + if (!c.edit) { this.showMissingPermissionsError(); return; } @@ -936,19 +938,12 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled(); - const canDeleteCollections = collections == null || - collections.every((c) => - c.canDelete( - organizations.find((o) => o.id == c.organizationId), - flexibleCollectionsV1Enabled, - ), - ); + collections.every((c) => c.canDelete(organizations.find((o) => o.id == c.organizationId))); const canDeleteCiphers = ciphers == null || ciphers.every((c) => c.edit); - if (flexibleCollectionsV1Enabled && (!canDeleteCollections || !canDeleteCiphers)) { + if (!canDeleteCollections || !canDeleteCiphers) { this.showMissingPermissionsError(); return; } @@ -1052,10 +1047,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - if ( - (await this.flexibleCollectionsV1Enabled()) && - ciphers.some((c) => c.organizationId != null) - ) { + if (ciphers.some((c) => c.organizationId != null)) { // You cannot move ciphers between organizations this.showMissingPermissionsError(); return; @@ -1099,10 +1091,8 @@ export class VaultComponent implements OnInit, OnDestroy { return true; } - const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled(); - const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId); - return organization.canEditAllCiphers(flexibleCollectionsV1Enabled, false); + return organization.canEditAllCiphers(false); } private go(queryParams: any = null) { @@ -1131,10 +1121,6 @@ export class VaultComponent implements OnInit, OnDestroy { message: this.i18nService.t("missingPermissions"), }); } - - private flexibleCollectionsV1Enabled() { - return firstValueFrom(this.flexibleCollectionsV1Enabled$); - } } /** diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index c0a83ed74cb..8fd15cf20e8 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -82,12 +82,7 @@ export class AddEditComponent extends BaseAddEditComponent { } protected loadCollections() { - if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) { return super.loadCollections(); } return Promise.resolve(this.collections); @@ -98,10 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent { const firstCipherCheck = await super.loadCipher(); if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) && + !this.organization.canEditAllCiphers(this.restrictProviderAccess) && firstCipherCheck != null ) { return firstCipherCheck; @@ -116,24 +108,14 @@ export class AddEditComponent extends BaseAddEditComponent { } protected encryptCipher() { - if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) { return super.encryptCipher(); } return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher); } protected async deleteCipher() { - if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) { return super.deleteCipher(); } return this.cipher.isDeleted diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 30189e80215..71e7842913b 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -28,7 +28,6 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On viewOnly = false; organization: Organization; - private flexibleCollectionsV1Enabled = false; private restrictProviderAccess = false; constructor( @@ -60,9 +59,6 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On async ngOnInit() { await super.ngOnInit(); - this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), - ); this.restrictProviderAccess = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess), ); @@ -70,10 +66,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected async reupload(attachment: AttachmentView) { if ( - this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) && + this.organization.canEditAllCiphers(this.restrictProviderAccess) && this.showFixOldAttachments(attachment) ) { await super.reuploadCipherAttachment(attachment, true); @@ -81,12 +74,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On } protected async loadCipher() { - if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -97,20 +85,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On return this.cipherService.saveAttachmentWithServer( this.cipherDomain, file, - this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ), + this.organization.canEditAllCiphers(this.restrictProviderAccess), ); } protected deleteCipherAttachment(attachmentId: string) { - if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) - ) { + if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); @@ -118,11 +98,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected showFixOldAttachments(attachment: AttachmentView) { return ( - attachment.key == null && - this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) + attachment.key == null && this.organization.canEditAllCiphers(this.restrictProviderAccess) ); } } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 557b048a7be..4ee052e32fe 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -61,10 +61,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected async loadCipher() { // if cipher is unassigned use apiService. We can see this by looking at this.collectionIds if ( - !this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) && + !this.organization.canEditAllCiphers(this.restrictProviderAccess) && this.collectionIds.length !== 0 ) { return await super.loadCipher(); @@ -89,10 +86,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected saveCollections() { if ( - this.organization.canEditAllCiphers( - this.flexibleCollectionsV1Enabled, - this.restrictProviderAccess, - ) || + this.organization.canEditAllCiphers(this.restrictProviderAccess) || this.collectionIds.length === 0 ) { const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 31764fcf058..56fb2e4cec8 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -87,7 +87,6 @@ export class VaultHeaderComponent implements OnInit { protected CollectionDialogTabType = CollectionDialogTabType; protected organizations$ = this.organizationService.organizations$; - protected flexibleCollectionsV1Enabled = false; protected restrictProviderAccessFlag = false; constructor( @@ -100,9 +99,6 @@ export class VaultHeaderComponent implements OnInit { ) {} async ngOnInit() { - this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), - ); this.restrictProviderAccessFlag = await this.configService.getFeatureFlag( FeatureFlag.RestrictProviderAccess, ); @@ -195,7 +191,7 @@ export class VaultHeaderComponent implements OnInit { } // Otherwise, check if we can edit the specified collection - return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled); + return this.collection.node.canEdit(this.organization); } addCipher() { @@ -225,14 +221,11 @@ export class VaultHeaderComponent implements OnInit { } // Otherwise, check if we can delete the specified collection - return this.collection.node.canDelete(this.organization, this.flexibleCollectionsV1Enabled); + return this.collection.node.canDelete(this.organization); } get canViewCollectionInfo(): boolean { - return this.collection.node.canViewCollectionInfo( - this.organization, - this.flexibleCollectionsV1Enabled, - ); + return this.collection.node.canViewCollectionInfo(this.organization); } get canCreateCollection(): boolean { diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 881b0948c01..b27c0234a58 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -68,39 +68,12 @@ [showBulkEditCollectionAccess]="true" [showBulkAddToCollections]="true" [viewingOrgVault]="true" - [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" [addAccessStatus]="addAccessStatus$ | async" [addAccessToggle]="showAddAccessToggle" [restrictProviderAccess]="restrictProviderAccessEnabled" > - -
    - -

    {{ "noPermissionToViewAllCollectionItems" | i18n }}

    -
    -
    - -

    {{ "noItemsInList" | i18n }}

    - -
    - + {{ "noItemsInList" | i18n }} - + {{ "autoFillOnPageLoad" | i18n }} diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts index 6f73ffabefb..601380f98a1 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.spec.ts @@ -32,6 +32,7 @@ describe("AutofillOptionsComponent", () => { autofillSettingsService = mock(); autofillSettingsService.autofillOnPageLoadDefault$ = new BehaviorSubject(false); + autofillSettingsService.autofillOnPageLoad$ = new BehaviorSubject(true); await TestBed.configureTestingModule({ imports: [AutofillOptionsComponent], @@ -145,6 +146,22 @@ describe("AutofillOptionsComponent", () => { expect(component["autofillOptions"][0].label).toEqual("defaultLabel yes"); }); + it("hides the autofill on page load field when the setting is disabled", () => { + fixture.detectChanges(); + let control = fixture.nativeElement.querySelector( + "bit-select[formControlName='autofillOnPageLoad']", + ); + expect(control).toBeTruthy(); + + (autofillSettingsService.autofillOnPageLoad$ as BehaviorSubject).next(false); + + fixture.detectChanges(); + control = fixture.nativeElement.querySelector( + "bit-select[formControlName='autofillOnPageLoad']", + ); + expect(control).toBeFalsy(); + }); + it("announces the addition of a new URI input", fakeAsync(() => { fixture.detectChanges(); diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index 389eda4c189..80de50c4421 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -70,6 +70,7 @@ export class AutofillOptionsComponent implements OnInit { } protected defaultMatchDetection$ = this.domainSettingsService.defaultUriMatchStrategy$; + protected autofillOnPageLoadEnabled$ = this.autofillSettingsService.autofillOnPageLoad$; protected autofillOptions: { label: string; value: boolean | null }[] = [ { label: this.i18nService.t("default"), value: null }, diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html index 470b2881aba..a55716083de 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html @@ -1,6 +1,6 @@ - - {{ "websiteUri" | i18n }} + + {{ uriLabel }} - + {{ "matchDetection" | i18n }} + + + + {{ "password" | i18n }} + + + {{ "passphrase" | i18n }} + + + + + + + + + + + + + + + + + + + + + + + +

    {{ "options" | i18n }}

    +
    + + Placeholder: Replace with Generator Options Component(s) when available + +
    diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts new file mode 100644 index 00000000000..5b65c6da24d --- /dev/null +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts @@ -0,0 +1,210 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + PasswordGenerationServiceAbstraction, + PasswordGeneratorOptions, + UsernameGenerationServiceAbstraction, + UsernameGeneratorOptions, +} from "@bitwarden/generator-legacy"; +import { CipherFormGeneratorComponent } from "@bitwarden/vault"; + +describe("CipherFormGeneratorComponent", () => { + let component: CipherFormGeneratorComponent; + let fixture: ComponentFixture; + + let mockLegacyPasswordGenerationService: MockProxy; + let mockLegacyUsernameGenerationService: MockProxy; + let mockPlatformUtilsService: MockProxy; + + let passwordOptions$: BehaviorSubject; + let usernameOptions$: BehaviorSubject; + + beforeEach(async () => { + passwordOptions$ = new BehaviorSubject([ + { + type: "password", + }, + ] as [PasswordGeneratorOptions]); + usernameOptions$ = new BehaviorSubject([ + { + type: "word", + }, + ] as [UsernameGeneratorOptions]); + + mockPlatformUtilsService = mock(); + + mockLegacyPasswordGenerationService = mock(); + mockLegacyPasswordGenerationService.getOptions$.mockReturnValue(passwordOptions$); + + mockLegacyUsernameGenerationService = mock(); + mockLegacyUsernameGenerationService.getOptions$.mockReturnValue(usernameOptions$); + + await TestBed.configureTestingModule({ + imports: [CipherFormGeneratorComponent], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { + provide: PasswordGenerationServiceAbstraction, + useValue: mockLegacyPasswordGenerationService, + }, + { + provide: UsernameGenerationServiceAbstraction, + useValue: mockLegacyUsernameGenerationService, + }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CipherFormGeneratorComponent); + component = fixture.componentInstance; + }); + + it("should create", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("should use the appropriate text based on generator type", () => { + component.type = "password"; + component.ngOnChanges(); + expect(component["regenerateButtonTitle"]).toBe("regeneratePassword"); + + component.type = "username"; + component.ngOnChanges(); + expect(component["regenerateButtonTitle"]).toBe("regenerateUsername"); + }); + + it("should emit regenerate$ when user clicks the regenerate button", fakeAsync(() => { + const regenerateSpy = jest.spyOn(component["regenerate$"], "next"); + + fixture.nativeElement.querySelector("button[data-testid='regenerate-button']").click(); + + expect(regenerateSpy).toHaveBeenCalled(); + })); + + it("should emit valueGenerated whenever a new value is generated", fakeAsync(() => { + const valueGeneratedSpy = jest.spyOn(component.valueGenerated, "emit"); + + mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("generated-password"); + component.type = "password"; + + component.ngOnChanges(); + tick(); + + expect(valueGeneratedSpy).toHaveBeenCalledWith("generated-password"); + })); + + describe("password generation", () => { + beforeEach(() => { + component.type = "password"; + }); + + it("should update the generated value when the password options change", fakeAsync(() => { + mockLegacyPasswordGenerationService.generatePassword + .mockResolvedValueOnce("first-password") + .mockResolvedValueOnce("second-password"); + + component.ngOnChanges(); + tick(); + + expect(component["generatedValue"]).toBe("first-password"); + + passwordOptions$.next([{ type: "password" }]); + tick(); + + expect(component["generatedValue"]).toBe("second-password"); + expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(2); + })); + + it("should show password type toggle when the generator type is password", () => { + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector("bit-toggle-group")).toBeTruthy(); + }); + + it("should save password options when the password type is updated", async () => { + mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("generated-password"); + + await component["updatePasswordType"]("passphrase"); + + expect(mockLegacyPasswordGenerationService.saveOptions).toHaveBeenCalledWith({ + type: "passphrase", + }); + }); + + it("should update the password history when a new password is generated", fakeAsync(() => { + mockLegacyPasswordGenerationService.generatePassword.mockResolvedValue("new-password"); + + component.ngOnChanges(); + tick(); + + expect(mockLegacyPasswordGenerationService.generatePassword).toHaveBeenCalledTimes(1); + expect(mockLegacyPasswordGenerationService.addHistory).toHaveBeenCalledWith("new-password"); + expect(component["generatedValue"]).toBe("new-password"); + })); + + it("should regenerate the password when regenerate$ emits", fakeAsync(() => { + mockLegacyPasswordGenerationService.generatePassword + .mockResolvedValueOnce("first-password") + .mockResolvedValueOnce("second-password"); + + component.ngOnChanges(); + tick(); + + expect(component["generatedValue"]).toBe("first-password"); + + component["regenerate$"].next(); + tick(); + + expect(component["generatedValue"]).toBe("second-password"); + })); + }); + + describe("username generation", () => { + beforeEach(() => { + component.type = "username"; + }); + + it("should update the generated value when the username options change", fakeAsync(() => { + mockLegacyUsernameGenerationService.generateUsername + .mockResolvedValueOnce("first-username") + .mockResolvedValueOnce("second-username"); + + component.ngOnChanges(); + tick(); + + expect(component["generatedValue"]).toBe("first-username"); + + usernameOptions$.next([{ type: "word" }]); + tick(); + + expect(component["generatedValue"]).toBe("second-username"); + })); + + it("should regenerate the username when regenerate$ emits", fakeAsync(() => { + mockLegacyUsernameGenerationService.generateUsername + .mockResolvedValueOnce("first-username") + .mockResolvedValueOnce("second-username"); + + component.ngOnChanges(); + tick(); + + expect(component["generatedValue"]).toBe("first-username"); + + component["regenerate$"].next(); + tick(); + + expect(component["generatedValue"]).toBe("second-username"); + })); + + it("should not show password type toggle when the generator type is username", () => { + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector("bit-toggle-group")).toBeNull(); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts new file mode 100644 index 00000000000..2d24194d290 --- /dev/null +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -0,0 +1,159 @@ +import { CommonModule } from "@angular/common"; +import { Component, DestroyRef, EventEmitter, Input, OnChanges, Output } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { firstValueFrom, map, startWith, Subject, Subscription, switchMap, tap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + CardComponent, + ColorPasswordModule, + IconButtonModule, + ItemModule, + SectionComponent, + SectionHeaderComponent, + ToggleGroupModule, + TypographyModule, +} from "@bitwarden/components"; +import { GeneratorType } from "@bitwarden/generator-core"; +import { + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; + +/** + * Renders a password or username generator UI and emits the most recently generated value. + * Used by the cipher form to be shown in a dialog/modal when generating cipher passwords/usernames. + */ +@Component({ + selector: "vault-cipher-form-generator", + templateUrl: "./cipher-form-generator.component.html", + standalone: true, + imports: [ + CommonModule, + CardComponent, + SectionComponent, + ToggleGroupModule, + JslibModule, + ItemModule, + ColorPasswordModule, + IconButtonModule, + SectionHeaderComponent, + TypographyModule, + ], +}) +export class CipherFormGeneratorComponent implements OnChanges { + /** + * The type of generator form to show. + */ + @Input({ required: true }) + type: "password" | "username"; + + /** + * Emits an event when a new value is generated. + */ + @Output() + valueGenerated = new EventEmitter(); + + protected get isPassword() { + return this.type === "password"; + } + + protected regenerateButtonTitle: string; + protected regenerate$ = new Subject(); + /** + * The currently generated value displayed to the user. + * @protected + */ + protected generatedValue: string = ""; + + /** + * The current password generation options. + * @private + */ + private passwordOptions$ = this.legacyPasswordGenerationService.getOptions$(); + + /** + * The current username generation options. + * @private + */ + private usernameOptions$ = this.legacyUsernameGenerationService.getOptions$(); + + /** + * The current password type specified by the password generation options. + * @protected + */ + protected passwordType$ = this.passwordOptions$.pipe(map(([options]) => options.type)); + + /** + * Tracks the regenerate$ subscription + * @private + */ + private subscription: Subscription | null; + + constructor( + private i18nService: I18nService, + private legacyPasswordGenerationService: PasswordGenerationServiceAbstraction, + private legacyUsernameGenerationService: UsernameGenerationServiceAbstraction, + private destroyRef: DestroyRef, + ) {} + + ngOnChanges() { + this.regenerateButtonTitle = this.i18nService.t( + this.isPassword ? "regeneratePassword" : "regenerateUsername", + ); + + // If we have a previous subscription, clear it + if (this.subscription) { + this.subscription.unsubscribe(); + this.subscription = null; + } + + if (this.isPassword) { + this.setupPasswordGeneration(); + } else { + this.setupUsernameGeneration(); + } + } + + private setupPasswordGeneration() { + this.subscription = this.regenerate$ + .pipe( + startWith(null), + switchMap(() => this.passwordOptions$), + switchMap(([options]) => this.legacyPasswordGenerationService.generatePassword(options)), + tap(async (password) => { + await this.legacyPasswordGenerationService.addHistory(password); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((password) => { + this.generatedValue = password; + this.valueGenerated.emit(password); + }); + } + + private setupUsernameGeneration() { + this.subscription = this.regenerate$ + .pipe( + startWith(null), + switchMap(() => this.usernameOptions$), + switchMap((options) => this.legacyUsernameGenerationService.generateUsername(options)), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((username) => { + this.generatedValue = username; + this.valueGenerated.emit(username); + }); + } + + /** + * Switch the password generation type and save the options (generating a new password automatically). + * @param value The new password generation type. + */ + protected updatePasswordType = async (value: GeneratorType) => { + const [currentOptions] = await firstValueFrom(this.passwordOptions$); + currentOptions.type = value; + await this.legacyPasswordGenerationService.saveOptions(currentOptions); + }; +} diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 435ba7b4eb8..26f7f7fd9e8 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -17,12 +17,8 @@ {{ "itemName" | i18n }}
    -
    - +
    + {{ "owner" | i18n }} - + {{ "folder" | i18n }} {{ "password" | i18n }} + + + {{ "securePasswordGenerated" | i18n }} + + + + {{ "useGeneratorHelpTextPartOne" | i18n }} {{ "useGeneratorHelpTextPartTwo" | i18n }} + + + + + [appA11yTitle]="'learnMoreAboutAuthenticators' | i18n" + > + +

    {{ (canCaptureTotp ? "totpHelperWithCapture" : "totpHelper") | i18n }}

    diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index a723fd7dc85..06f325d0534 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -125,14 +125,6 @@ describe("LoginDetailsSectionComponent", () => { }); }); - it("initializes 'loginDetailsForm' with generated password when creating a new cipher", async () => { - generationService.generateInitialPassword.mockResolvedValue("generated-password"); - - await component.ngOnInit(); - - expect(component.loginDetailsForm.controls.password.value).toBe("generated-password"); - }); - describe("viewHiddenFields", () => { beforeEach(() => { (cipherFormContainer.originalCipherView as CipherView) = { diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 839b9512105..020c2d18bd8 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -14,6 +14,7 @@ import { CardComponent, FormFieldModule, IconButtonModule, + LinkModule, PopoverModule, SectionComponent, SectionHeaderComponent, @@ -43,6 +44,7 @@ import { AutofillOptionsComponent } from "../autofill-options/autofill-options.c NgIf, PopoverModule, AutofillOptionsComponent, + LinkModule, ], }) export class LoginDetailsSectionComponent implements OnInit { @@ -52,6 +54,11 @@ export class LoginDetailsSectionComponent implements OnInit { totp: [""], }); + /** + * Flag indicating whether a new password has been generated for the current form. + */ + newPasswordGenerated: boolean; + /** * Whether the TOTP field can be captured from the current tab. Only available in the browser extension. */ @@ -148,7 +155,7 @@ export class LoginDetailsSectionComponent implements OnInit { private async initNewCipher() { this.loginDetailsForm.patchValue({ username: this.cipherFormContainer.config.initialValues?.username || "", - password: await this.generationService.generateInitialPassword(), + password: "", }); } @@ -193,6 +200,7 @@ export class LoginDetailsSectionComponent implements OnInit { if (newPassword) { this.loginDetailsForm.controls.password.patchValue(newPassword); + this.newPasswordGenerated = true; } }; diff --git a/libs/vault/src/cipher-form/index.ts b/libs/vault/src/cipher-form/index.ts index 1d275029df1..8cb779a8ec3 100644 --- a/libs/vault/src/cipher-form/index.ts +++ b/libs/vault/src/cipher-form/index.ts @@ -8,3 +8,4 @@ export { export { TotpCaptureService } from "./abstractions/totp-capture.service"; export { CipherFormGenerationService } from "./abstractions/cipher-form-generation.service"; export { DefaultCipherFormConfigService } from "./services/default-cipher-form-config.service"; +export { CipherFormGeneratorComponent } from "./components/cipher-generator/cipher-form-generator.component"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-generation.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-generation.service.ts index 181590e8418..b20a1dba3b9 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-generation.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-generation.service.ts @@ -23,8 +23,4 @@ export class DefaultCipherFormGenerationService implements CipherFormGenerationS const options = await this.usernameGenerationService.getOptions(); return await this.usernameGenerationService.generateUsername(options); } - - async generateInitialPassword(): Promise { - return await this.generatePassword(); - } } diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.html b/libs/vault/src/cipher-view/additional-options/additional-options.component.html index 59b2753945a..6f254b8c729 100644 --- a/libs/vault/src/cipher-view/additional-options/additional-options.component.html +++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.html @@ -2,7 +2,7 @@

    {{ "additionalOptions" | i18n }}

    - + {{ "note" | i18n }} diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html new file mode 100644 index 00000000000..65a80009ce0 --- /dev/null +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.html @@ -0,0 +1,30 @@ + + +

    {{ "autofillOptions" | i18n }}

    +
    + + + + + {{ "website" | i18n }} + + + + + + + +
    diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts new file mode 100644 index 00000000000..84f25a146c6 --- /dev/null +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts @@ -0,0 +1,40 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { + CardComponent, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + IconButtonModule, +} from "@bitwarden/components"; + +import { TotpCaptureService } from "../../cipher-form"; + +@Component({ + selector: "app-autofill-options-view", + templateUrl: "autofill-options-view.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + FormFieldModule, + IconButtonModule, + ], +}) +export class AutofillOptionsViewComponent { + @Input() loginUris: LoginUriView[]; + + constructor(private totpCaptureService: TotpCaptureService) {} + + async openWebsite(selectedUri: string) { + await this.totpCaptureService.openAutofillNewTab(selectedUri); + } +} diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html index 588849eaee4..c446ba4f319 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html @@ -2,8 +2,8 @@

    {{ setSectionTitle }}

    - - + + {{ "cardholderName" | i18n }} - + {{ "number" | i18n }} - + {{ "expiration" | i18n }} - + {{ "securityCode" | i18n }} + + + + + + + - - - + diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index d2ac0963911..092b7dba396 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -13,16 +13,14 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { SearchModule } from "@bitwarden/components"; -import { PopupFooterComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-footer.component"; -import { PopupHeaderComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-header.component"; -import { PopupPageComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-page.component"; - import { AdditionalOptionsComponent } from "./additional-options/additional-options.component"; import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component"; +import { AutofillOptionsViewComponent } from "./autofill-options/autofill-options-view.component"; import { CardDetailsComponent } from "./card-details/card-details-view.component"; import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component"; import { ItemDetailsV2Component } from "./item-details/item-details-v2.component"; import { ItemHistoryV2Component } from "./item-history/item-history-v2.component"; +import { LoginCredentialsViewComponent } from "./login-credentials/login-credentials-view.component"; import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component"; @Component({ @@ -33,9 +31,6 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide CommonModule, SearchModule, JslibModule, - PopupPageComponent, - PopupHeaderComponent, - PopupFooterComponent, ItemDetailsV2Component, AdditionalOptionsComponent, AttachmentsV2ViewComponent, @@ -43,6 +38,8 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide CustomFieldV2Component, CardDetailsComponent, ViewIdentitySectionsComponent, + LoginCredentialsViewComponent, + AutofillOptionsViewComponent, ], }) export class CipherViewComponent implements OnInit, OnDestroy { @@ -61,6 +58,7 @@ export class CipherViewComponent implements OnInit, OnDestroy { async ngOnInit() { await this.loadCipherData(); } + ngOnDestroy(): void { this.destroyed$.next(); this.destroyed$.complete(); @@ -71,6 +69,15 @@ export class CipherViewComponent implements OnInit, OnDestroy { return cardholderName || code || expMonth || expYear || brand || number; } + get hasLogin() { + const { username, password, totp } = this.cipher.login; + return username || password || totp; + } + + get hasAutofill() { + return this.cipher.login?.uris.length > 0; + } + async loadCipherData() { if (this.cipher.collectionIds.length > 0) { this.collections$ = this.collectionService diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html index 3bee39d5195..df9731570c7 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -22,6 +22,7 @@